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/add_context_commits_modal/store/actions.js2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue7
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue92
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/labels_select.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue104
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue81
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue48
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_details.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue4
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js2
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql)1
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql (renamed from app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql)0
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql30
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql3
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql18
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql18
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql8
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql10
-rw-r--r--app/assets/javascripts/admin/background_migrations/index.js2
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue7
-rw-r--r--app/assets/javascripts/admin/users/components/actions/index.js4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/trust_user.vue62
-rw-r--r--app/assets/javascripts/admin/users/components/actions/untrust_user.vue56
-rw-r--r--app/assets/javascripts/admin/users/components/app.vue63
-rw-r--r--app/assets/javascripts/admin/users/constants.js6
-rw-r--r--app/assets/javascripts/alert.js2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js4
-rw-r--r--app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js28
-rw-r--r--app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue45
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_tile.vue1
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue2
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/users_chart.vue2
-rw-r--r--app/assets/javascripts/api/bulk_imports_api.js15
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue6
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue247
-rw-r--r--app/assets/javascripts/batch_comments/queries/can_approve.query.graphql11
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js18
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/load_startup_css.js15
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js7
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue21
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue34
-rw-r--r--app/assets/javascripts/blob/components/constants.js3
-rw-r--r--app/assets/javascripts/blob/filepath_form/components/template_selector.vue5
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue6
-rw-r--r--app/assets/javascripts/boards/boards_util.js4
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue1
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue3
-rw-r--r--app/assets/javascripts/boards/components/new_board_button.vue2
-rw-r--r--app/assets/javascripts/boards/constants.js1
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js8
-rw-r--r--app/assets/javascripts/boards/stores/index.js15
-rw-r--r--app/assets/javascripts/branches/components/branch_more_actions.vue1
-rw-r--r--app/assets/javascripts/branches/components/delete_branch_modal.vue1
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue3
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue37
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue14
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue112
-rw-r--r--app/assets/javascripts/ci/catalog/global_catalog.vue10
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql16
-rw-r--r--app/assets/javascripts/ci/catalog/index.js37
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue12
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue511
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue28
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue8
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue10
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue14
-rw-r--r--app/assets/javascripts/ci/common/private/job_links_layer.vue10
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue2
-rw-r--r--app/assets/javascripts/ci/constants.js1
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue6
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue3
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue4
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue15
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js2
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js45
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue4
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue6
-rw-r--r--app/assets/javascripts/ci/jobs_page/jobs_page_app.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue20
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue17
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue28
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue39
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue42
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue60
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue36
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue97
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue13
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue14
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue220
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue28
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue11
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/constants.js3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue14
-rw-r--r--app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue20
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue33
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue41
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_created_at.vue72
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue18
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue29
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_header.vue17
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js12
-rw-r--r--app/assets/javascripts/ci/runner/constants.js7
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql10
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql5
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue18
-rw-r--r--app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue4
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js10
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js2
-rw-r--r--app/assets/javascripts/ci/utils.js2
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue2
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue8
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js2
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js2
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue2
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/copy_paste.js40
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js3
-rw-r--r--app/assets/javascripts/content_editor/extensions/loading.js56
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js27
-rw-r--r--app/assets/javascripts/content_editor/extensions/word_break.js2
-rw-r--r--app/assets/javascripts/content_editor/services/content_editor.js2
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue10
-rw-r--r--app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql (renamed from app/assets/javascripts/crm/organizations/components/graphql/update_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/delete_item.vue2
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue2
-rw-r--r--app/assets/javascripts/deprecated_notes.js2
-rw-r--r--app/assets/javascripts/design_management/components/delete_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue3
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue2
-rw-r--r--app/assets/javascripts/design_management/components/list/item.vue17
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue6
-rw-r--r--app/assets/javascripts/diffs/components/app.vue127
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue5
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql82
-rw-r--r--app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql46
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer.vue165
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue30
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue140
-rw-r--r--app/assets/javascripts/diffs/components/tree_list_height.vue108
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js13
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js7
-rw-r--r--app/assets/javascripts/diffs/store/utils.js2
-rw-r--r--app/assets/javascripts/diffs/utils/file_reviews.js2
-rw-r--r--app/assets/javascripts/diffs/utils/sort_findings_by_file.js11
-rw-r--r--app/assets/javascripts/editor/constants.js5
-rw-r--r--app/assets/javascripts/editor/schema/ci.json12
-rw-r--r--app/assets/javascripts/emoji/awards_app/store/actions.js2
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue4
-rw-r--r--app/assets/javascripts/emoji/constants.js12
-rw-r--r--app/assets/javascripts/emoji/index.js107
-rw-r--r--app/assets/javascripts/ensure_data.js2
-rw-r--r--app/assets/javascripts/entrypoints/analytics.js6
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue1
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue2
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue30
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue16
-rw-r--r--app/assets/javascripts/environments/constants.js4
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue2
-rw-r--r--app/assets/javascripts/environments/graphql/client.js1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql15
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql10
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql2
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/base.js4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/flux.js116
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js51
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue6
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue1
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js19
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js23
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js2
-rw-r--r--app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue2
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js1
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql10
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue1
-rw-r--r--app/assets/javascripts/groups/settings/init_access_dropdown.js2
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue6
-rw-r--r--app/assets/javascripts/header.js3
-rw-r--r--app/assets/javascripts/header_search/index.js2
-rw-r--r--app/assets/javascripts/header_search/init.js2
-rw-r--r--app/assets/javascripts/helpers/help_page_helper.js2
-rw-r--r--app/assets/javascripts/helpers/init_simple_app_helper.js29
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue1
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue9
-rw-r--r--app/assets/javascripts/ide/lib/alerts/index.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/file_templates/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/mutations.js2
-rw-r--r--app/assets/javascripts/import/constants.js12
-rw-r--r--app/assets/javascripts/import/details/components/bulk_import_details_app.vue44
-rw-r--r--app/assets/javascripts/import/details/components/import_details_app.vue38
-rw-r--r--app/assets/javascripts/import/details/components/import_details_table.vue96
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue57
-rw-r--r--app/assets/javascripts/import_entities/constants.js53
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_status.vue83
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue21
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue12
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js2
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue3
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue2
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue2
-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.vue80
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue10
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue12
-rw-r--r--app/assets/javascripts/invite_members/components/project_select.vue1
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js3
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js4
-rw-r--r--app/assets/javascripts/issuable/components/locked_badge.vue9
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue11
-rw-r--r--app/assets/javascripts/issuable/components/status_badge.vue10
-rw-r--r--app/assets/javascripts/issuable/popover/components/mr_popover.vue4
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue5
-rw-r--r--app/assets/javascripts/issues/issue.js2
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue12
-rw-r--r--app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue259
-rw-r--r--app/assets/javascripts/issues/show/components/issue_header.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue14
-rw-r--r--app/assets/javascripts/issues/show/utils/parse_data.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue58
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/color_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js15
-rw-r--r--app/assets/javascripts/lib/utils/constants.js3
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js16
-rw-r--r--app/assets/javascripts/lib/utils/forms.js22
-rw-r--r--app/assets/javascripts/lib/utils/keys.js1
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/members/components/avatars/group_avatar.vue48
-rw-r--r--app/assets/javascripts/members/components/icons/private_icon.vue19
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue20
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue1
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue70
-rw-r--r--app/assets/javascripts/members/store/actions.js5
-rw-r--r--app/assets/javascripts/members/utils.js34
-rw-r--r--app/assets/javascripts/merge_request_tabs.js7
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue37
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue206
-rw-r--r--app/assets/javascripts/milestones/stores/mutations.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js7
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue8
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js4
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue61
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue59
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue16
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_row.vue (renamed from app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue)16
-rw-r--r--app/assets/javascripts/ml/model_registry/components/search_bar.vue71
-rw-r--r--app/assets/javascripts/ml/model_registry/constants.js13
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue49
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/routes/models/index/translations.js16
-rw-r--r--app/assets/javascripts/ml/model_registry/translations.js16
-rw-r--r--app/assets/javascripts/mr_notes/init.js1
-rw-r--r--app/assets/javascripts/notes/components/comment_field_layout.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue4
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_button.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue5
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue2
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js5
-rw-r--r--app/assets/javascripts/observability/client.js133
-rw-r--r--app/assets/javascripts/observability/components/loader/constants.js20
-rw-r--r--app/assets/javascripts/observability/components/loader/index.vue (renamed from app/assets/javascripts/observability/components/skeleton/index.vue)80
-rw-r--r--app/assets/javascripts/observability/components/observability_container.vue58
-rw-r--r--app/assets/javascripts/observability/components/observability_empty_state.vue36
-rw-r--r--app/assets/javascripts/observability/components/provisioned_observability_container.vue95
-rw-r--r--app/assets/javascripts/observability/constants.js24
-rw-r--r--app/assets/javascripts/organizations/mock_data.js27
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue18
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/new/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/new/index.js3
-rw-r--r--app/assets/javascripts/organizations/profile/preferences/index.js41
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/app.vue14
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue77
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/settings/general/index.js38
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue117
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js3
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql9
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js6
-rw-r--r--app/assets/javascripts/organizations/users/components/app.vue51
-rw-r--r--app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql17
-rw-r--r--app/assets/javascripts/organizations/users/index.js29
-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/pages/list.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue9
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue13
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js3
-rw-r--r--app/assets/javascripts/pages/explore/catalog/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/details/index.js20
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue39
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/index.js3
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue14
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/users/index.js3
-rw-r--r--app/assets/javascripts/pages/organizations/settings/general/index.js3
-rw-r--r--app/assets/javascripts/pages/passwords/new/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/preferences/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue50
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js3
-rw-r--r--app/assets/javascripts/pages/projects/ml/model_versions/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/product_analytics/graphs/index.js3
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/users/index.js11
-rw-r--r--app/assets/javascripts/pages/users/show/index.js5
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js5
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue5
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/wrapper.vue3
-rw-r--r--app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue3
-rw-r--r--app/assets/javascripts/pipeline_wizard/templates/pages.yml20
-rw-r--r--app/assets/javascripts/profile/edit/components/profile_edit_app.vue7
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue6
-rw-r--r--app/assets/javascripts/profile/profile.js8
-rw-r--r--app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue7
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue12
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js2
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js2
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue6
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue12
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue4
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue1
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue1
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js3
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue2
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue4
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue57
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue13
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue59
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue11
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js14
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue25
-rw-r--r--app/assets/javascripts/releases/components/release_block_header.vue2
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue7
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue17
-rw-r--r--app/assets/javascripts/repository/components/commit_info.vue18
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue13
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue24
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js11
-rw-r--r--app/assets/javascripts/search/sidebar/components/blobs_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue5
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue61
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue7
-rw-r--r--app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js2
-rw-r--r--app/assets/javascripts/search/store/getters.js39
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue2
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js6
-rw-r--r--app/assets/javascripts/sentry/legacy_index.js2
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js16
-rw-r--r--app/assets/javascripts/service_ping_consent.js35
-rw-r--r--app/assets/javascripts/sessions/new/components/update_email.vue4
-rw-r--r--app/assets/javascripts/sessions/new/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue25
-rw-r--r--app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue210
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue10
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js3
-rw-r--r--app/assets/javascripts/sidebar/queries/constants.js4
-rw-r--r--app/assets/javascripts/silent_mode_settings/components/app.vue10
-rw-r--r--app/assets/javascripts/single_file_diff.js5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/flyout_menu.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue9
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue27
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue22
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js12
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js2
-rw-r--r--app/assets/javascripts/tags/components/delete_tag_modal.vue1
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue11
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue8
-rw-r--r--app/assets/javascripts/time_tracking/components/timelogs_app.vue2
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue11
-rw-r--r--app/assets/javascripts/token_access/graphql/cache_config.js14
-rw-r--r--app/assets/javascripts/token_access/index.js3
-rw-r--r--app/assets/javascripts/tracking/constants.js3
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js2
-rw-r--r--app/assets/javascripts/users/profile/actions/components/user_actions_app.vue8
-rw-r--r--app/assets/javascripts/users/profile/components/report_abuse_button.vue58
-rw-r--r--app/assets/javascripts/users/profile/index.js23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js85
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue220
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue118
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue (renamed from app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js)51
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue19
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.query.graphql5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue4
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/chronic_duration_input.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue157
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue128
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/constants.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue150
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/project_select.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue106
-rw-r--r--app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/form/errors_alert.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/group_item.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/index.vue193
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/user_item.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/blame_info.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql36
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_labels.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue (renamed from app/assets/javascripts/admin/users/components/user_avatar.vue)21
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/users_table.vue (renamed from app/assets/javascripts/admin/users/components/users_table.vue)68
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue4
-rw-r--r--app/assets/javascripts/vue_shared/directives/safe_html.js2
-rw-r--r--app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js11
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue17
-rw-r--r--app/assets/javascripts/webhooks/components/push_events.vue8
-rw-r--r--app/assets/javascripts/work_items/components/notes/system_note.vue22
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue10
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue7
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue11
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue5
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue46
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue28
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue126
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue104
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_award_emoji.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue132
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue11
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue134
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent.vue58
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue28
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle.vue (renamed from app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue)44
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_todos.vue9
-rw-r--r--app/assets/javascripts/work_items/constants.js27
-rw-r--r--app/assets/javascripts/work_items/graphql/award_emoji.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_items.query.graphql17
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql4
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue2
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue2
615 files changed, 7846 insertions, 4532 deletions
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index f085b0d0e5e..890db374160 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,5 +1,5 @@
import _ from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
index 3c46de7c2be..f0540ffa71e 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue
@@ -7,6 +7,7 @@ import ReportDetails from './report_details.vue';
import ReportedContent from './reported_content.vue';
import ActivityEventsList from './activity_events_list.vue';
import ActivityHistoryItem from './activity_history_item.vue';
+import AbuseReportNotes from './abuse_report_notes.vue';
const alertDefaults = {
visible: false,
@@ -24,6 +25,7 @@ export default {
ReportedContent,
ActivityEventsList,
ActivityHistoryItem,
+ AbuseReportNotes,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -96,5 +98,10 @@ export default {
/>
</template>
</activity-events-list>
+
+ <abuse-report-notes
+ v-if="glFeatures.abuseReportNotes"
+ :abuse-report-id="abuseReport.report.globalId"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
new file mode 100644
index 00000000000..80af7d7400a
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
@@ -0,0 +1,92 @@
+<script>
+import { uniqueId } from 'lodash';
+import { __ } from '~/locale';
+import { createAlert } from '~/alert';
+import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
+import { SKELETON_NOTES_COUNT } from '~/admin/abuse_report/constants';
+import abuseReportNotesQuery from '../graphql/notes/abuse_report_notes.query.graphql';
+import AbuseReportDiscussion from './notes/abuse_report_discussion.vue';
+
+export default {
+ name: 'AbuseReportNotes',
+ SKELETON_NOTES_COUNT,
+ i18n: {
+ fetchError: __('An error occurred while fetching comments, please try again.'),
+ },
+ components: {
+ SkeletonLoadingContainer,
+ AbuseReportDiscussion,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ addNoteKey: uniqueId(`abuse-report-add-note-${this.abuseReportId}`),
+ };
+ },
+ apollo: {
+ abuseReportNotes: {
+ query: abuseReportNotesQuery,
+ variables() {
+ return {
+ id: this.abuseReportId,
+ };
+ },
+ update(data) {
+ return data.abuseReport?.discussions || [];
+ },
+ skip() {
+ return !this.abuseReportId;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ initialLoading() {
+ return this.$apollo.queries.abuseReportNotes.loading;
+ },
+ notesArray() {
+ return this.abuseReportNotes?.nodes || [];
+ },
+ },
+ methods: {
+ getDiscussionKey(discussion) {
+ const discussionId = discussion.notes.nodes[0].id;
+ return discussionId.split('/')[discussionId.split('/').length - 1];
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="issuable-discussion gl-mb-5 gl-clearfix!">
+ <template v-if="initialLoading">
+ <ul class="notes main-notes-list timeline">
+ <skeleton-loading-container
+ v-for="index in $options.SKELETON_NOTES_COUNT"
+ :key="index"
+ class="note-skeleton"
+ />
+ </ul>
+ </template>
+
+ <template v-else>
+ <ul class="notes main-notes-list timeline">
+ <abuse-report-discussion
+ v-for="discussion in notesArray"
+ :key="getDiscussionKey(discussion)"
+ :discussion="discussion.notes.nodes"
+ :abuse-report-id="abuseReportId"
+ />
+ </ul>
+ </template>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
index 8c4c1da28b8..2206e600543 100644
--- a/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
@@ -11,7 +11,7 @@ export default {
<!-- The styles `issuable-discussion`, `timeline`, `main-notes-list` and `notes` used below
are declared in app/assets/stylesheets/pages/notes.scss -->
<section class="gl-pt-6 issuable-discussion">
- <h2 class="gl-font-lg gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
+ <h2 class="gl-font-size-h1 gl-mt-0 gl-mb-4">{{ $options.i18n.activity }}</h2>
<ul class="timeline main-notes-list notes">
<slot name="history-items"></slot>
</ul>
diff --git a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
index 747c9a1a947..d2d143f0460 100644
--- a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
@@ -11,7 +11,7 @@ import DropdownContentsCreateView from '~/sidebar/components/labels/labels_selec
import DropdownHeader from '~/sidebar/components/labels/labels_select_widget/dropdown_header.vue';
import DropdownFooter from '~/sidebar/components/labels/labels_select_widget/dropdown_footer.vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
-import abuseReportLabelsQuery from './graphql/abuse_report_labels.query.graphql';
+import abuseReportLabelsQuery from '../graphql/abuse_report_labels.query.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
new file mode 100644
index 00000000000..4d24471fa43
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
@@ -0,0 +1,104 @@
+<script>
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import AbuseReportNote from './abuse_report_note.vue';
+
+export default {
+ name: 'AbuseReportDiscussion',
+ components: {
+ TimelineEntryItem,
+ DiscussionNotesRepliesWrapper,
+ ToggleRepliesWidget,
+ AbuseReportNote,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ discussion: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isExpanded: true,
+ };
+ },
+ computed: {
+ note() {
+ return this.discussion[0];
+ },
+ noteId() {
+ return getIdFromGraphQLId(this.note.id);
+ },
+ replies() {
+ if (this.discussion?.length > 1) {
+ return this.discussion.slice(1);
+ }
+ return null;
+ },
+ hasReplies() {
+ return Boolean(this.replies?.length);
+ },
+ discussionId() {
+ return this.discussion[0]?.discussion?.id || '';
+ },
+ },
+ methods: {
+ toggleDiscussion() {
+ this.isExpanded = !this.isExpanded;
+ },
+ },
+};
+</script>
+
+<template>
+ <abuse-report-note
+ v-if="!hasReplies"
+ :note="note"
+ :abuse-report-id="abuseReportId"
+ class="gl-mb-4"
+ />
+ <timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
+ <div class="timeline-content">
+ <div class="discussion">
+ <div class="discussion-body">
+ <div class="discussion-wrapper">
+ <div class="discussion-notes">
+ <ul class="notes">
+ <abuse-report-note
+ :note="note"
+ :discussion-id="discussionId"
+ :abuse-report-id="abuseReportId"
+ class="gl-mb-4"
+ />
+ <discussion-notes-replies-wrapper>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="!isExpanded"
+ :replies="replies"
+ @toggle="toggleDiscussion({ discussionId })"
+ />
+ <template v-if="isExpanded">
+ <template v-for="reply in replies">
+ <abuse-report-note
+ :key="reply.id"
+ :discussion-id="discussionId"
+ :note="reply"
+ :abuse-report-id="abuseReportId"
+ />
+ </template>
+ </template>
+ </discussion-notes-replies-wrapper>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
new file mode 100644
index 00000000000..6da3017e11e
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import NoteBody from './abuse_report_note_body.vue';
+
+export default {
+ name: 'AbuseReportNote',
+ directives: {
+ SafeHtml,
+ },
+ components: {
+ GlAvatarLink,
+ GlAvatar,
+ TimelineEntryItem,
+ NoteHeader,
+ NoteBody,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ noteAnchorId() {
+ return `note_${getIdFromGraphQLId(this.note.id)}`;
+ },
+ author() {
+ return this.note.author;
+ },
+ authorId() {
+ return getIdFromGraphQLId(this.author.id);
+ },
+ },
+};
+</script>
+
+<template>
+ <timeline-entry-item :id="noteAnchorId" class="note note-wrapper note-comment">
+ <div :key="note.id" class="timeline-avatar gl-float-left">
+ <gl-avatar-link
+ :href="author.webUrl"
+ :data-user-id="authorId"
+ :data-username="author.username"
+ class="js-user-link"
+ >
+ <gl-avatar
+ :src="author.avatarUrl"
+ :entity-name="author.username"
+ :alt="author.name"
+ :size="32"
+ />
+ </gl-avatar-link>
+ </div>
+ <div class="timeline-content">
+ <div data-testid="note-wrapper">
+ <div class="note-header">
+ <note-header
+ :author="author"
+ :created-at="note.createdAt"
+ :note-id="note.id"
+ :note-url="note.url"
+ >
+ <span v-if="note.createdAt" class="d-none d-sm-inline">&middot;</span>
+ </note-header>
+ </div>
+
+ <div class="timeline-discussion-body">
+ <note-body ref="noteBody" :note="note" />
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue
new file mode 100644
index 00000000000..ab3d7f5fa6c
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_body.vue
@@ -0,0 +1,48 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+export default {
+ name: 'AbuseReportNoteBody',
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ watch: {
+ 'note.bodyHtml': {
+ immediate: true,
+ async handler(newVal, oldVal) {
+ if (newVal === oldVal) {
+ return;
+ }
+ await this.$nextTick();
+ this.renderGFM();
+ },
+ },
+ },
+ methods: {
+ renderGFM() {
+ renderGFM(this.$refs['note-body']);
+ gl?.lazyLoader?.searchLazyImages();
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
+ },
+};
+</script>
+
+<template>
+ <div ref="note-body" class="note-body">
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="note.bodyHtml"
+ class="note-text md"
+ data-testid="abuse-report-note-body"
+ ></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
index 10e1dca7f91..89017e6cbd4 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
@@ -1,8 +1,8 @@
<script>
import { __ } from '~/locale';
import { createAlert } from '~/alert';
+import abuseReportQuery from '../graphql/abuse_report.query.graphql';
import LabelsSelect from './labels_select.vue';
-import abuseReportQuery from './graphql/abuse_report.query.graphql';
export default {
name: 'ReportDetails',
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index 84d6f25ac05..99c8b3ece10 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -67,7 +67,7 @@ export default {
<div
class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center"
>
- <h2 class="gl-font-lg gl-mt-2 gl-mb-2">
+ <h2 class="gl-font-size-h1 gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
@@ -128,7 +128,7 @@ export default {
</gl-link>
<time-ago-tooltip
:time="report.reportedAt"
- class="gl-ml-3 gl-text-secondary gl-xs-w-full"
+ class="gl-ml-3 gl-text-secondary gl-w-full gl-sm-w-auto"
/>
</div>
</div>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index f028408bed7..c56ea678b1d 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -111,3 +111,5 @@ export const HISTORY_ITEMS_I18N = {
reportedByForCategory: s__('AbuseReport|Reported by %{name} for %{category}.'),
deletedReporter: s__('AbuseReport|No user found'),
};
+
+export const SKELETON_NOTES_COUNT = 5;
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql
index f5b075cb9af..640eec718f8 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report.query.graphql
@@ -1,5 +1,6 @@
query abuseReportQuery($id: AbuseReportID!) {
abuseReport(id: $id) {
+ id
labels {
nodes {
id
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql
index 4e724b4db2c..4e724b4db2c 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/abuse_report_labels.query.graphql
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql
index 0781b8e634b..0781b8e634b 100644
--- a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql
new file mode 100644
index 00000000000..84b57b4ed79
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note.fragment.graphql
@@ -0,0 +1,30 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+#import "./abuse_report_note_permissions.fragment.graphql"
+
+fragment AbuseReportNote on Note {
+ id
+ body
+ bodyHtml
+ createdAt
+ lastEditedAt
+ url
+ resolved
+ author {
+ ...Author
+ }
+ lastEditedBy {
+ ...Author
+ webPath
+ }
+ userPermissions {
+ ...AbuseReportNotePermissions
+ }
+ discussion {
+ id
+ notes {
+ nodes {
+ id
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
new file mode 100644
index 00000000000..01436436b93
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
@@ -0,0 +1,3 @@
+fragment AbuseReportNotePermissions on NotePermissions {
+ adminNote
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql
new file mode 100644
index 00000000000..3a13ac1f37a
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_notes.query.graphql
@@ -0,0 +1,18 @@
+#import "./abuse_report_note.fragment.graphql"
+
+query abuseReportNotes($id: AbuseReportID!) {
+ abuseReport(id: $id) {
+ id
+ discussions {
+ nodes {
+ id
+ replyId
+ notes {
+ nodes {
+ ...AbuseReportNote
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql
new file mode 100644
index 00000000000..53ac9468e08
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/create_abuse_report_note.mutation.graphql
@@ -0,0 +1,18 @@
+#import "./abuse_report_note.fragment.graphql"
+
+mutation createAbuseReportNote($input: CreateNoteInput!) {
+ createNote(input: $input) {
+ note {
+ id
+ discussion {
+ id
+ notes {
+ nodes {
+ ...AbuseReportNote
+ }
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql
new file mode 100644
index 00000000000..e8ff2933159
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/delete_abuse_report_note.fragment.graphql
@@ -0,0 +1,8 @@
+mutation deleteAbuseReportNote($input: DestroyNoteInput!) {
+ destroyNote(input: $input) {
+ errors
+ note {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql
new file mode 100644
index 00000000000..e11165074c9
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/update_abuse_report_note.mutation.graphql
@@ -0,0 +1,10 @@
+#import "./abuse_report_note.fragment.graphql"
+
+mutation updateAbuseReportNote($input: UpdateNoteInput!) {
+ updateNote(input: $input) {
+ note {
+ ...AbuseReportNote
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/background_migrations/index.js b/app/assets/javascripts/admin/background_migrations/index.js
index 4ddd8f17c9a..890df17080d 100644
--- a/app/assets/javascripts/admin/background_migrations/index.js
+++ b/app/assets/javascripts/admin/background_migrations/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Translate from '~/vue_shared/translate';
import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue';
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
index 2c555aca3c0..753b1fb1819 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -107,7 +107,7 @@ export default {
targetSelected: '',
targetPath: this.broadcastMessage.targetPath,
targetAccessLevels: this.broadcastMessage.targetAccessLevels,
- targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
+ targetAccessLevelCheckBoxGroupOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
text,
value,
})),
@@ -324,7 +324,10 @@ export default {
:state="!isValidated || targetRolesValid"
data-testid="target-roles-checkboxes"
>
- <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" />
+ <gl-form-checkbox-group
+ v-model="targetAccessLevels"
+ :options="targetAccessLevelCheckBoxGroupOptions"
+ />
</gl-form-group>
<gl-form-group
diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js
index 4e63a85df89..633bc4d8b15 100644
--- a/app/assets/javascripts/admin/users/components/actions/index.js
+++ b/app/assets/javascripts/admin/users/components/actions/index.js
@@ -9,6 +9,8 @@ import Reject from './reject.vue';
import Unban from './unban.vue';
import Unblock from './unblock.vue';
import Unlock from './unlock.vue';
+import Trust from './trust_user.vue';
+import Untrust from './untrust_user.vue';
export default {
Activate,
@@ -22,4 +24,6 @@ export default {
Unblock,
Unlock,
Reject,
+ Trust,
+ Untrust,
};
diff --git a/app/assets/javascripts/admin/users/components/actions/trust_user.vue b/app/assets/javascripts/admin/users/components/actions/trust_user.vue
new file mode 100644
index 00000000000..41ff8d4120d
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/trust_user.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|When not being monitored for spam:')}</p>
+ <ul>
+ <li>${s__(
+ 'AdminUsers|The user can create issues, notes, snippets, and merge requests that appear to be spam without being blocked.',
+ )}</li>
+ </ul>
+ <p>${s__('AdminUsers|You can untrust this user in the future.')}</p>
+`;
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
+ title: sprintf(s__('AdminUsers|Stop monitoring %{username} for possible spam?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.trust,
+ attributes: { variant: 'confirm' },
+ },
+ messageHtml,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <slot></slot>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/untrust_user.vue b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue
new file mode 100644
index 00000000000..da59833af07
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/untrust_user.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from '~/vue_shared/components/confirm_modal_eventhub';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `<p>${s__(
+ 'AdminUsers|You can trust this user in the future if necessary.',
+)}</p>`;
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ onClick() {
+ eventHub.$emit(EVENT_OPEN_CONFIRM_MODAL, {
+ path: this.path,
+ method: 'put',
+ modalAttributes: {
+ title: sprintf(s__('AdminUsers|Re-enable spam monitoring for %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.untrust,
+ attributes: { variant: 'confirm' },
+ },
+ messageHtml,
+ },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown-item @action="onClick">
+ <template #list-item>
+ <slot></slot>
+ </template>
+ </gl-disclosure-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/app.vue b/app/assets/javascripts/admin/users/components/app.vue
index a3abd904a6b..b0caffb6ca6 100644
--- a/app/assets/javascripts/admin/users/components/app.vue
+++ b/app/assets/javascripts/admin/users/components/app.vue
@@ -1,9 +1,15 @@
<script>
-import UsersTable from './users_table.vue';
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
+import UserActions from './user_actions.vue';
export default {
components: {
UsersTable,
+ UserActions,
},
props: {
users: {
@@ -16,11 +22,64 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ groupCounts: {},
+ };
+ },
+ apollo: {
+ groupCounts: {
+ query: getUsersGroupCountsQuery,
+ variables() {
+ return {
+ usernames: this.users.map((user) => user.username),
+ };
+ },
+ update(data) {
+ const nodes = data?.users?.nodes || [];
+ const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
+
+ return parsedIds.reduce((acc, { id, groupCount }) => {
+ acc[id] = groupCount || 0;
+ return acc;
+ }, {});
+ },
+ error(error) {
+ createAlert({
+ message: this.$options.i18n.groupCountFetchError,
+ captureError: true,
+ error,
+ });
+ },
+ skip() {
+ return !this.users.length;
+ },
+ },
+ },
+ computed: {
+ groupCountsLoading() {
+ return this.$apollo.queries.groupCounts.loading;
+ },
+ },
+ i18n: {
+ groupCountFetchError: s__(
+ 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
+ ),
+ },
};
</script>
<template>
<div>
- <users-table :users="users" :paths="paths" />
+ <users-table
+ :users="users"
+ :admin-user-path="paths.adminUser"
+ :group-counts="groupCounts"
+ :group-counts-loading="groupCountsLoading"
+ >
+ <template #user-actions="{ user }">
+ <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ </template>
+ </users-table>
</div>
</template>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index 9cd61d6b1db..73383623aa2 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -1,9 +1,5 @@
import { s__, __ } from '~/locale';
-export const USER_AVATAR_SIZE = 32;
-
-export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
-
export const I18N_USER_ACTIONS = {
edit: __('Edit'),
userAdministration: s__('AdminUsers|User administration'),
@@ -19,4 +15,6 @@ export const I18N_USER_ACTIONS = {
deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
ban: s__('AdminUsers|Ban user'),
unban: s__('AdminUsers|Unban user'),
+ trust: s__('AdminUsers|Trust user'),
+ untrust: s__('AdminUsers|Untrust user'),
};
diff --git a/app/assets/javascripts/alert.js b/app/assets/javascripts/alert.js
index 4d724b17723..fd20d216385 100644
--- a/app/assets/javascripts/alert.js
+++ b/app/assets/javascripts/alert.js
@@ -1,7 +1,7 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
import isEmpty from 'lodash/isEmpty';
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
export const VARIANT_SUCCESS = 'success';
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index fb872243e5e..29156a624fd 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -13,8 +13,8 @@ import {
GlTabs,
GlTab,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { isEqual, isEmpty, omit } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { PROMO_URL, DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
import {
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 4fa88279fe0..d1c8d2c24e7 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { formatMedianValues } from '../utils';
-import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants';
+import { PAGINATION_SORT_DIRECTION_DESC, PAGINATION_SORT_FIELD_DURATION } from '../constants';
import * as types from './mutation_types';
export default {
@@ -41,7 +41,7 @@ export default {
Vue.set(state, 'pagination', {
page,
hasNextPage,
- sort: sort || PAGINATION_SORT_FIELD_END_EVENT,
+ sort: sort || PAGINATION_SORT_FIELD_DURATION,
direction: direction || PAGINATION_SORT_DIRECTION_DESC,
});
},
diff --git a/app/assets/javascripts/analytics/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 3d9b56b043d..f387bf65093 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -1,5 +1,5 @@
import {
- PAGINATION_SORT_FIELD_END_EVENT,
+ PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/analytics/cycle_analytics/constants';
@@ -29,7 +29,7 @@ export default () => ({
pagination: {
page: null,
hasNextPage: false,
- sort: PAGINATION_SORT_FIELD_END_EVENT,
+ sort: PAGINATION_SORT_FIELD_DURATION,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
predefinedDateRange: null,
diff --git a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js b/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
deleted file mode 100644
index 91cb48e181b..00000000000
--- a/app/assets/javascripts/analytics/product_analytics/activity_charts_bundle.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Vue from 'vue';
-import ActivityChart from './components/activity_chart.vue';
-
-export default () => {
- const containers = document.querySelectorAll('.js-project-analytics-chart');
-
- if (!containers) {
- return false;
- }
-
- return containers.forEach((container) => {
- const { chartData } = container.dataset;
- const formattedData = JSON.parse(chartData);
-
- return new Vue({
- el: container,
- components: {
- ActivityChart,
- },
- provide: {
- formattedData,
- },
- render(createElement) {
- return createElement('activity-chart');
- },
- });
- });
-};
diff --git a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
deleted file mode 100644
index 2be9ebda87a..00000000000
--- a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<script>
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { s__ } from '~/locale';
-
-export default {
- i18n: {
- noDataMsg: s__(
- 'ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already.',
- ),
- },
- components: {
- GlColumnChart,
- },
- inject: {
- formattedData: {
- default: {},
- },
- },
- computed: {
- barSeriesData() {
- return [
- {
- name: 'full',
- data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
- },
- ];
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-xs-w-full">
- <gl-column-chart
- v-if="formattedData.keys"
- :bars="barSeriesData"
- :x-axis-title="__('Value')"
- :y-axis-title="__('Number of events')"
- :x-axis-type="'category'"
- />
- <p v-else data-testid="noActivityChartData">
- {{ $options.i18n.noDataMsg }}
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/analytics/shared/components/metric_tile.vue b/app/assets/javascripts/analytics/shared/components/metric_tile.vue
index 54dbe329c7a..9e0262b5175 100644
--- a/app/assets/javascripts/analytics/shared/components/metric_tile.vue
+++ b/app/assets/javascripts/analytics/shared/components/metric_tile.vue
@@ -44,6 +44,7 @@ export default {
:animation-decimal-places="decimalPlaces"
:class="{ 'gl-hover-cursor-pointer': hasLinks }"
tabindex="0"
+ use-delimiters
@click="clickHandler(metric)"
/>
<metric-popover :metric="metric" :target="metric.identifier" />
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
index 8d7761694d1..247c147609b 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_trends_count_chart.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
-import * as Sentry from '@sentry/browser';
import { some, every } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
differenceInMonths,
formatDateAsMonth,
diff --git a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
index 06b83c87985..47a34ec8b4d 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/users_chart.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { formatDateAsMonth } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue';
diff --git a/app/assets/javascripts/api/bulk_imports_api.js b/app/assets/javascripts/api/bulk_imports_api.js
index d636cfdff0b..248f5601705 100644
--- a/app/assets/javascripts/api/bulk_imports_api.js
+++ b/app/assets/javascripts/api/bulk_imports_api.js
@@ -2,6 +2,21 @@ import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
+const BULK_IMPORT_ENTITIES_FAILURES_PATH =
+ '/api/:version/bulk_imports/:id/entities/:entity_id/failures';
export const getBulkImportsHistory = (params) =>
axios.get(buildApiUrl(BULK_IMPORT_ENTITIES_PATH), { params });
+
+export const getBulkImportFailures = (id, entityId, { page, perPage }) => {
+ const failuresPath = buildApiUrl(BULK_IMPORT_ENTITIES_FAILURES_PATH)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':entity_id', encodeURIComponent(entityId));
+
+ return axios.get(failuresPath, {
+ params: {
+ page,
+ per_page: perPage,
+ },
+ });
+};
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 2be59f00773..19da1253a17 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -277,11 +277,7 @@ export default {
>
{{ saveText }}
</gl-button>
- <gl-button
- :type="cancelButtonType"
- data-qa-selector="cancel_badge_button"
- @click="handleCancel"
- >
+ <gl-button :type="cancelButtonType" @click="handleCancel">
{{ __('Cancel') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index fac45f32464..b5cb1862b45 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -1,39 +1,69 @@
<script>
-import { GlDropdown, GlButton, GlIcon, GlForm, GlFormCheckbox } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlButton,
+ GlIcon,
+ GlForm,
+ GlFormCheckbox,
+ GlFormRadioGroup,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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 { fetchPolicies } from '~/lib/graphql';
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';
+import userCanApproveQuery from '../queries/can_approve.query.graphql';
export default {
+ apollo: {
+ userPermissions: {
+ fetchPolicy: fetchPolicies.NETWORK_ONLY,
+ query: userCanApproveQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath.replace(/^\//, ''),
+ iid: `${this.getNoteableData.iid}`,
+ };
+ },
+ update: (data) => data.project?.mergeRequest?.userPermissions,
+ skip() {
+ return !this.dropdownVisible;
+ },
+ },
+ },
components: {
- GlDropdown,
+ GlDisclosureDropdown,
GlButton,
GlIcon,
GlForm,
+ GlFormRadioGroup,
GlFormCheckbox,
MarkdownEditor,
ApprovalPassword: () => import('ee_component/batch_comments/components/approval_password.vue'),
SummarizeMyReview: () =>
import('ee_component/batch_comments/components/summarize_my_review.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
canSummarize: { default: false },
},
data() {
return {
isSubmitting: false,
+ dropdownVisible: false,
noteData: {
noteable_type: '',
noteable_id: '',
note: '',
approve: false,
approval_password: '',
+ reviewer_state: 'reviewed',
},
formFieldProps: {
id: 'review-note-body',
@@ -42,17 +72,51 @@ export default {
'aria-label': __('Comment'),
'data-testid': 'comment-textarea',
},
+ userPermissions: {},
};
},
computed: {
...mapGetters(['getNotesData', 'getNoteableData', 'noteableType', 'getCurrentUserLastNote']),
...mapState('batchComments', ['shouldAnimateReviewButton']),
+ ...mapState('diffs', ['projectPath']),
autocompleteDataSources() {
return gl.GfmAutoComplete?.dataSources;
},
autosaveKey() {
return `submit_review_dropdown/${this.getNoteableData.id}`;
},
+ radioGroupOptions() {
+ return [
+ {
+ html: [
+ __('Comment'),
+ `<p class="help-text">
+ ${__('Submit general feedback without explicit approval.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'reviewed',
+ },
+ {
+ html: [
+ __('Approve'),
+ `<p class="help-text">
+ ${__('Submit feedback and approve these changes.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'approved',
+ disabled: !this.userPermissions.canApprove,
+ },
+ {
+ html: [
+ __('Request changes'),
+ `<p class="help-text">
+ ${__('Submit feedback that should be addressed before merging.')}
+ </p>`,
+ ].join('<br />'),
+ value: 'requested_changes',
+ },
+ ];
+ },
},
watch: {
'noteData.approve': function noteDataApproveWatch() {
@@ -60,21 +124,21 @@ export default {
this.repositionDropdown();
});
},
+ dropdownVisible(val) {
+ if (!val) {
+ this.userPermissions = {};
+ }
+ },
+ userPermissions: {
+ handler() {
+ this.repositionDropdown();
+ },
+ deep: true,
+ },
},
mounted() {
this.noteData.noteable_type = this.noteableType;
this.noteData.noteable_id = this.getNoteableData.id;
-
- // We override the Bootstrap Vue click outside behaviour
- // to allow for clicking in the autocomplete dropdowns
- // without this override the submit dropdown will close
- // whenever a item in the autocomplete dropdown is clicked
- const originalClickOutHandler = this.$refs.submitDropdown.$refs.dropdown.clickOutHandler;
- this.$refs.submitDropdown.$refs.dropdown.clickOutHandler = (e) => {
- if (!e.target.closest('.atwho-container')) {
- originalClickOutHandler(e);
- }
- };
},
methods: {
...mapActions('batchComments', ['publishReview']),
@@ -113,86 +177,115 @@ export default {
updateNote(note) {
this.noteData.note = note;
},
+ onBeforeClose({ originalEvent: { target }, preventDefault }) {
+ if (
+ target &&
+ [document.querySelector('.atwho-container'), document.querySelector('.dz-hidden-input')]
+ .filter(Boolean)
+ .some((el) => el.contains(target))
+ ) {
+ preventDefault();
+ }
+ },
+ setDropdownVisible(val) {
+ this.dropdownVisible = val;
+ },
},
restrictedToolbarItems: ['full-screen'],
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
ref="submitDropdown"
- right
- dropup
+ placement="right"
class="submit-review-dropdown"
:class="{ 'submit-review-dropdown-animated': shouldAnimateReviewButton }"
data-testid="submit-review-dropdown"
- variant="info"
- category="primary"
+ fluid-width
+ @beforeClose="onBeforeClose"
+ @shown="setDropdownVisible(true)"
+ @hidden="setDropdownVisible(false)"
>
- <template #button-content>
- {{ __('Finish review') }}
- <gl-icon class="dropdown-chevron" name="chevron-up" />
+ <template #toggle>
+ <gl-button variant="info" category="primary">
+ {{ __('Finish review') }}
+ <gl-icon class="dropdown-chevron" name="chevron-up" />
+ </gl-button>
</template>
- <gl-form data-testid="submit-gl-form" @submit.prevent="submitReview">
- <div class="gl-display-flex gl-mb-4 gl-align-items-center">
- <label for="review-note-body" class="gl-mb-0">
- {{ __('Summary comment (optional)') }}
- </label>
- <summarize-my-review
- v-if="canSummarize"
- :id="getNoteableData.id"
- class="gl-ml-auto"
- @input="updateNote"
- />
- </div>
- <div class="common-note-form gfm-form">
- <markdown-editor
- ref="markdownEditor"
- v-model="noteData.note"
- class="js-no-autosize"
- :is-submitting="isSubmitting"
- :render-markdown-path="getNoteableData.preview_note_path"
- :markdown-docs-path="getNotesData.markdownDocsPath"
- :form-field-props="formFieldProps"
- enable-autocomplete
- :autocomplete-data-sources="autocompleteDataSources"
- :disabled="isSubmitting"
- :restricted-tool-bar-items="$options.restrictedToolbarItems"
- :force-autosize="false"
- :autosave-key="autosaveKey"
- supports-quick-actions
- @input="$emit('input', $event)"
- @keydown.meta.enter="submitReview"
- @keydown.ctrl.enter="submitReview"
- />
- </div>
- <template v-if="getNoteableData.current_user.can_approve">
- <gl-form-checkbox
- v-model="noteData.approve"
- data-testid="approve_merge_request"
+ <template #default>
+ <gl-form
+ class="submit-review-dropdown-form gl-p-4"
+ data-testid="submit-gl-form"
+ @submit.prevent="submitReview"
+ >
+ <div class="gl-display-flex gl-mb-4 gl-align-items-center">
+ <label for="review-note-body" class="gl-mb-0">
+ {{ __('Summary comment (optional)') }}
+ </label>
+ <summarize-my-review
+ v-if="canSummarize"
+ :id="getNoteableData.id"
+ class="gl-ml-auto"
+ @input="updateNote"
+ />
+ </div>
+ <div class="common-note-form gfm-form">
+ <markdown-editor
+ ref="markdownEditor"
+ v-model="noteData.note"
+ class="js-no-autosize"
+ :is-submitting="isSubmitting"
+ :render-markdown-path="getNoteableData.preview_note_path"
+ :markdown-docs-path="getNotesData.markdownDocsPath"
+ :form-field-props="formFieldProps"
+ enable-autocomplete
+ :autocomplete-data-sources="autocompleteDataSources"
+ :disabled="isSubmitting"
+ :restricted-tool-bar-items="$options.restrictedToolbarItems"
+ :force-autosize="false"
+ :autosave-key="autosaveKey"
+ supports-quick-actions
+ @input="$emit('input', $event)"
+ @keydown.meta.enter="submitReview"
+ @keydown.ctrl.enter="submitReview"
+ />
+ </div>
+ <gl-form-radio-group
+ v-if="glFeatures.mrRequestChanges"
+ v-model="noteData.reviewer_state"
+ :options="radioGroupOptions"
class="gl-mt-4"
- >
- {{ __('Approve merge request') }}
- </gl-form-checkbox>
+ data-testid="reviewer_states"
+ />
+ <template v-else-if="userPermissions.canApprove">
+ <gl-form-checkbox
+ v-model="noteData.approve"
+ data-testid="approve_merge_request"
+ class="gl-mt-4"
+ >
+ {{ __('Approve merge request') }}
+ </gl-form-checkbox>
+ </template>
<approval-password
- v-if="getNoteableData.require_password_to_approve"
- v-show="noteData.approve"
+ v-if="userPermissions.canApprove && getNoteableData.require_password_to_approve"
+ v-show="noteData.approve || noteData.reviewer_state === 'approved'"
v-model="noteData.approval_password"
class="gl-mt-3"
data-testid="approve_password"
/>
- </template>
- <div class="gl-display-flex gl-justify-content-start gl-mt-4">
- <gl-button
- :loading="isSubmitting"
- variant="confirm"
- type="submit"
- class="js-no-auto-disable"
- data-testid="submit-review-button"
- >
- {{ __('Submit review') }}
- </gl-button>
- </div>
- </gl-form>
- </gl-dropdown>
+ <div class="gl-display-flex gl-justify-content-start gl-mt-4">
+ <gl-button
+ :loading="isSubmitting"
+ variant="confirm"
+ type="submit"
+ class="js-no-auto-disable"
+ data-testid="submit-review-button"
+ >
+ {{ __('Submit review') }}
+ </gl-button>
+ </div>
+ </gl-form>
+ </template>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql
new file mode 100644
index 00000000000..f0c9ef7b3c8
--- /dev/null
+++ b/app/assets/javascripts/batch_comments/queries/can_approve.query.graphql
@@ -0,0 +1,11 @@
+query userCanApprove($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ userPermissions {
+ canApprove
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index 070ce38c8aa..d97f11a0acd 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -72,22 +72,20 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
}),
);
-export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
+export const publishSingleDraft = ({ commit, getters }, draftId) => {
commit(types.REQUEST_PUBLISH_DRAFT, draftId);
service
.publishDraft(getters.getNotesData.draftsPublishPath, draftId)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_DRAFT_SUCCESS, draftId))
.catch(() => commit(types.RECEIVE_PUBLISH_DRAFT_ERROR, draftId));
};
-export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
+export const publishReview = ({ commit, getters }, noteData = {}) => {
commit(types.REQUEST_PUBLISH_REVIEW);
return service
.publish(getters.getNotesData.draftsPublishPath, noteData)
- .then(() => dispatch('updateDiscussionsAfterPublish'))
.then(() => commit(types.RECEIVE_PUBLISH_REVIEW_SUCCESS))
.catch((e) => {
commit(types.RECEIVE_PUBLISH_REVIEW_ERROR);
@@ -96,18 +94,6 @@ export const publishReview = ({ commit, dispatch, getters }, noteData = {}) => {
});
};
-export const updateDiscussionsAfterPublish = async ({ dispatch, getters, rootGetters }) => {
- await dispatch(
- 'fetchDiscussions',
- { path: getters.getNotesData.discussionsPath },
- { root: true },
- );
-
- dispatch('diffs/assignDiscussionsToDiff', rootGetters.discussionsStructuredByLineCode, {
- root: true,
- });
-};
-
export const updateDraft = (
{ commit, getters },
{ note, noteText, resolveDiscussion, position, flashContainer, callback, errorCallback },
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index dc9153e61f7..84ff8fa7f33 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -3,7 +3,6 @@ import './autosize';
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
import installGlEmojiElement from './gl_emoji';
-import { loadStartupCSS } from './load_startup_css';
import initCopyAsGFM from './markdown/copy_as_gfm';
import './quick_submit';
import './requires_input';
@@ -13,8 +12,6 @@ import { initGlobalAlerts } from './global_alerts';
import './toggler_behavior';
import './preview_markdown';
-loadStartupCSS();
-
installGlEmojiElement();
initCopyAsGFM();
diff --git a/app/assets/javascripts/behaviors/load_startup_css.js b/app/assets/javascripts/behaviors/load_startup_css.js
deleted file mode 100644
index dbe9ff8b6e7..00000000000
--- a/app/assets/javascripts/behaviors/load_startup_css.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export const loadStartupCSS = () => {
- // We need to fallback to dispatching `load` in case our event listener was added too late
- // or the browser environment doesn't load media=print.
- // Do this on `window.load` so that the default deferred behavior takes precedence.
- // https://gitlab.com/gitlab-org/gitlab/-/issues/239357
- window.addEventListener(
- 'load',
- () => {
- document
- .querySelectorAll('link[media=print]')
- .forEach((x) => x.dispatchEvent(new Event('load')));
- },
- { once: true },
- );
-};
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index e8c486f6e74..941662635ea 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -381,6 +381,12 @@ export const PROJECT_FILES_GO_TO_PERMALINK = {
defaultKeys: ['y'],
};
+export const PROJECT_FILES_GO_TO_COMPARE = {
+ id: 'projectFiles.goToCompare',
+ description: __('Compare Branches'),
+ defaultKeys: ['shift+c'],
+};
+
export const ISSUABLE_COMMENT_OR_REPLY = {
id: 'issuables.commentReply',
description: __('Comment/Reply (quoting selected text)'),
@@ -606,6 +612,7 @@ const PROJECT_FILES_SHORTCUTS_GROUP = {
PROJECT_FILES_OPEN_SELECTION,
PROJECT_FILES_GO_BACK,
PROJECT_FILES_GO_TO_PERMALINK,
+ PROJECT_FILES_GO_TO_COMPARE,
],
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index d9dc3aae808..4691a4228e6 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -18,6 +18,7 @@ import {
GO_TO_PROJECT_KUBERNETES,
GO_TO_PROJECT_ENVIRONMENTS,
GO_TO_PROJECT_WEBIDE,
+ PROJECT_FILES_GO_TO_COMPARE,
NEW_ISSUE,
} from './keybindings';
import Shortcuts from './shortcuts';
@@ -43,6 +44,7 @@ export default class ShortcutsNavigation extends Shortcuts {
[GO_TO_PROJECT_SNIPPETS, () => findAndFollowLink('.shortcuts-snippets')],
[GO_TO_PROJECT_KUBERNETES, () => findAndFollowLink('.shortcuts-kubernetes')],
[GO_TO_PROJECT_ENVIRONMENTS, () => findAndFollowLink('.shortcuts-environments')],
+ [PROJECT_FILES_GO_TO_COMPARE, () => findAndFollowLink('.shortcuts-compare')],
[GO_TO_PROJECT_WEBIDE, ShortcutsNavigation.navigateToWebIDE],
[NEW_ISSUE, () => findAndFollowLink('.shortcuts-new-issue')],
]);
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 699a0491183..5411881a8d2 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -5,7 +5,7 @@ import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
-import { SIMPLE_BLOB_VIEWER } from './constants';
+import { SIMPLE_BLOB_VIEWER, BLAME_VIEWER } from './constants';
import TableOfContents from './table_contents.vue';
export default {
@@ -85,6 +85,11 @@ export default {
required: false,
default: '',
},
+ showBlameToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -93,9 +98,6 @@ export default {
};
},
computed: {
- showViewerSwitcher() {
- return !this.hideViewerSwitcher && Boolean(this.blob.simpleViewer && this.blob.richViewer);
- },
showDefaultActions() {
return !this.hideDefaultActions;
},
@@ -114,7 +116,7 @@ export default {
},
watch: {
viewer(newVal, oldVal) {
- if (!this.hideViewerSwitcher && newVal !== oldVal) {
+ if (newVal !== BLAME_VIEWER && newVal !== oldVal) {
this.$emit('viewer-changed', newVal);
}
},
@@ -138,7 +140,14 @@ export default {
</div>
<div class="gl-display-flex gl-flex-wrap file-actions">
- <viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
+ <viewer-switcher
+ v-if="!hideViewerSwitcher"
+ v-model="viewer"
+ :doc-icon="blobSwitcherDocIcon"
+ :show-blame-toggle="showBlameToggle"
+ :show-viewer-toggles="Boolean(blob.simpleViewer && blob.richViewer)"
+ v-on="$listeners"
+ />
<web-ide-link
v-if="showWebIdeLink"
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index 7351df0f93b..9b5b77ebebe 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -5,6 +5,8 @@ import {
RICH_BLOB_VIEWER_TITLE,
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
+ BLAME_VIEWER,
+ BLAME_TITLE,
} from './constants';
export default {
@@ -26,6 +28,16 @@ export default {
default: 'document',
required: false,
},
+ showViewerToggles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showBlameToggle: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
isSimpleViewer() {
@@ -34,9 +46,16 @@ export default {
isRichViewer() {
return this.value === RICH_BLOB_VIEWER;
},
+ isBlameViewer() {
+ return this.value === BLAME_VIEWER;
+ },
},
methods: {
switchToViewer(viewer) {
+ if (viewer === BLAME_VIEWER) {
+ this.$emit('blame');
+ }
+
if (viewer !== this.value) {
this.$emit('input', viewer);
}
@@ -46,11 +65,14 @@ export default {
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
RICH_BLOB_VIEWER_TITLE,
+ BLAME_TITLE,
+ BLAME_VIEWER,
};
</script>
<template>
<gl-button-group class="js-blob-viewer-switcher mx-2">
<gl-button
+ v-if="showViewerToggles"
v-gl-tooltip.hover
:aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
@@ -63,6 +85,7 @@ export default {
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
/>
<gl-button
+ v-if="showViewerToggles"
v-gl-tooltip.hover
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
:title="$options.RICH_BLOB_VIEWER_TITLE"
@@ -74,5 +97,16 @@ export default {
data-viewer="rich"
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
/>
+ <gl-button
+ v-if="showBlameToggle"
+ v-gl-tooltip.hover
+ :title="$options.BLAME_TITLE"
+ :selected="isBlameViewer"
+ category="primary"
+ variant="default"
+ data-test-id="blame-toggle"
+ @click="switchToViewer($options.BLAME_VIEWER)"
+ >{{ __('Blame') }}</gl-button
+ >
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/blob/components/constants.js b/app/assets/javascripts/blob/components/constants.js
index adac4d6408d..bccab09c7a2 100644
--- a/app/assets/javascripts/blob/components/constants.js
+++ b/app/assets/javascripts/blob/components/constants.js
@@ -11,6 +11,9 @@ export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source');
export const RICH_BLOB_VIEWER = 'rich';
export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file');
+export const BLAME_VIEWER = 'blame';
+export const BLAME_TITLE = __('Display blame info');
+
export const BLOB_RENDER_EVENT_LOAD = 'force-content-fetch';
export const BLOB_RENDER_EVENT_SHOW_SOURCE = 'force-switch-viewer';
diff --git a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
index 51c69590796..379d5e38197 100644
--- a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
+++ b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
@@ -2,6 +2,7 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import SuggestGitlabCiYml from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import { __ } from '~/locale';
+import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
const templateSelectors = [
{
@@ -12,8 +13,8 @@ const templateSelectors = [
},
{
key: 'gitlab_ci_ymls',
- name: '.gitlab-ci.yml',
- pattern: /(.gitlab-ci.yml)/,
+ name: DEFAULT_CI_CONFIG_PATH,
+ pattern: CI_CONFIG_PATH_EXTENSION,
type: 'gitlab_ci_ymls',
},
{
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index f3c542c467a..0cc75d28e0b 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -94,14 +94,12 @@ export default {
<gl-modal visible size="sm" modal-id="success-pipeline-modal-id-not-used">
<template #modal-title>
{{ $options.i18n.modalTitle }}
- <gl-emoji class="gl-vertical-align-baseline font-size-inherit gl-mr-1" data-name="tada" />
+ <gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" data-name="tada" />
</template>
<p>
<gl-sprintf :message="$options.i18n.bodyMessage">
<template #codeQualityLink="{ content }">
- <gl-link :href="codeQualityLink" target="_blank" class="font-size-inherit">{{
- content
- }}</gl-link>
+ <gl-link :href="codeQualityLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index bf77aa4996c..fd36eea95eb 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -204,7 +204,8 @@ export function moveItemListHelper(item, fromList, toList) {
export function moveItemVariables({
iid,
- epicId,
+ itemId,
+ epicId = null,
fromListId,
toListId,
moveBeforeId,
@@ -225,6 +226,7 @@ export function moveItemVariables({
};
}
return {
+ itemId,
epicId,
boardId,
moveBeforeId,
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index 419d0b41d69..a3c0553d17c 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -43,7 +43,6 @@ export default {
<div
class="board-add-new-list board gl-display-inline-block gl-h-full gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0 gl-rounded-base gl-px-3"
data-testid="board-add-new-column"
- data-qa-selector="board_add_new_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_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index c10ff2e08da..a7f46dc9325 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -139,8 +139,11 @@ export default {
}
return false;
},
+ hasChildren() {
+ return this.totalIssuesCount + this.totalEpicsCount > 0;
+ },
shouldRenderEpicCountables() {
- return this.isEpicBoard && this.item.hasIssues;
+ return this.isEpicBoard && this.hasChildren;
},
shouldRenderEpicProgress() {
return this.totalWeight > 0;
@@ -396,7 +399,7 @@ export default {
<issue-due-date
v-if="item.dueDate"
:date="item.dueDate"
- :closed="item.closed || Boolean(item.closedAt)"
+ :closed="Boolean(item.closedAt)"
/>
<issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
<issue-card-weight
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 554f3bfa416..a6ff1653c17 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -249,7 +249,6 @@ export default {
<transition name="slide" @after-enter="afterFormEnters">
<board-add-new-column
v-if="addColumnFormVisible"
- class="gl-xs-w-full!"
:board-id="boardId"
:list-query-variables="listQueryVariables"
:lists="boardListsById"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 2693a6bb5ea..ca10cbbad5e 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -100,9 +100,6 @@ export default {
filters: this.filterParams,
};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return this.isEpicBoard;
},
@@ -123,9 +120,6 @@ export default {
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
@@ -149,9 +143,6 @@ export default {
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
@@ -400,7 +391,7 @@ export default {
this.updateIssueOrderInProgress = true;
await this.moveBoardItem(
{
- epicId: itemId,
+ itemId,
iid: itemIid,
fromListId: from.dataset.listId,
toListId: to.dataset.listId,
@@ -428,11 +419,11 @@ export default {
return items.some((item) => item.iid === itemIid);
},
async moveBoardItem(variables, newIndex) {
- const { fromListId, toListId, iid } = variables;
+ const { fromListId, toListId, iid, itemId } = variables;
this.toListId = toListId;
await this.$nextTick(); // we need this next tick to retrieve `toList` from Apollo cache
- const itemToMove = this.boardListItems.find((item) => item.iid === iid);
+ const itemToMove = this.boardListItems.find((item) => item.id === itemId);
if (this.shouldCloneCard && this.isItemInTheList(iid)) {
return;
@@ -445,6 +436,7 @@ export default {
...moveItemVariables({
...variables,
isIssue: !this.isEpicBoard,
+ epicId: itemId, // for Epic Boards
boardId: this.boardId,
itemToMove,
}),
@@ -532,7 +524,8 @@ export default {
variables: {
...moveItemVariables({
iid: item.iid,
- epicId: item.id,
+ itemId: item.id,
+ epicId: item.id, // for Epic Boards
fromListId: this.currentList.id,
toListId: this.currentList.id,
isIssue: !this.isEpicBoard,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 0235edd69ac..bedb3a75a70 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -223,9 +223,6 @@ export default {
variables() {
return this.countQueryVariables;
},
- context: {
- isSingleRequest: true,
- },
error(error) {
setError({
error,
diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue
index f7914c636cc..96cf0fadd6a 100644
--- a/app/assets/javascripts/boards/components/new_board_button.vue
+++ b/app/assets/javascripts/boards/components/new_board_button.vue
@@ -38,7 +38,7 @@ export default {
<template #control> </template>
<template #candidate>
<div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center">
- <gl-button data-qa-selector="new_board_button" @click.prevent="showDialog">
+ <gl-button @click.prevent="showDialog">
{{ createButtonText }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index cb607e5220e..acf01a8c528 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -132,6 +132,7 @@ export const listIssuablesQueries = {
optimisticResponse: {
assignees: { nodes: [], __typename: 'UserCoreConnection' },
confidential: false,
+ closedAt: null,
dueDate: null,
emailsDisabled: false,
hidden: false,
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index ea099e02181..bd58f445493 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -1,5 +1,6 @@
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
+import { toNumber } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { defaultClient } from '~/graphql_shared/issuable_client';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { listsDeferredQuery } from 'ee_else_ce/boards/constants';
@@ -83,7 +84,9 @@ export function updateIssueCountAndWeight({
boardList: {
...boardList,
issuesCount: boardList.issuesCount + 1,
- ...(issue.weight ? { totalIssueWeight: boardList.totalIssueWeight + issue.weight } : {}),
+ ...(issue.weight
+ ? { totalIssueWeight: toNumber(boardList.totalIssueWeight) + issue.weight }
+ : {}),
},
}),
);
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 3e7d7a7a8d3..97e40c8cc39 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import { sortBy } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
ListType,
inactiveId,
@@ -148,9 +148,6 @@ export default {
query: listsQuery[issuableType].query,
variables,
...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
- context: {
- isSingleRequest: true,
- },
})
.then(({ data }) => {
const { lists, hideBacklogList } = data[boardType].board;
@@ -439,9 +436,6 @@ export default {
return gqlClient
.query({
query: listsIssuesQuery,
- context: {
- isSingleRequest: true,
- },
variables,
...(!fetchNext ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
})
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
index ee0a5e27d9a..fd562df1df7 100644
--- a/app/assets/javascripts/boards/stores/index.js
+++ b/app/assets/javascripts/boards/stores/index.js
@@ -8,12 +8,13 @@ import state from 'ee_else_ce/boards/stores/state';
Vue.use(Vuex);
-export const createStore = () =>
- new Vuex.Store({
- state,
- getters,
- actions,
- mutations,
- });
+export const storeOptions = {
+ state,
+ getters,
+ actions,
+ mutations,
+};
+
+export const createStore = (options = storeOptions) => new Vuex.Store(options);
export default createStore();
diff --git a/app/assets/javascripts/branches/components/branch_more_actions.vue b/app/assets/javascripts/branches/components/branch_more_actions.vue
index c646dab2760..ee47f6af2f8 100644
--- a/app/assets/javascripts/branches/components/branch_more_actions.vue
+++ b/app/assets/javascripts/branches/components/branch_more_actions.vue
@@ -74,7 +74,6 @@ export default {
class: 'js-delete-branch-button gl-text-red-500!',
'aria-label': this.deleteBranchText,
'data-testid': 'delete-branch-button',
- 'data-qa-selector': 'delete_branch_button',
},
});
}
diff --git a/app/assets/javascripts/branches/components/delete_branch_modal.vue b/app/assets/javascripts/branches/components/delete_branch_modal.vue
index d5631337cec..0200a30cbdf 100644
--- a/app/assets/javascripts/branches/components/delete_branch_modal.vue
+++ b/app/assets/javascripts/branches/components/delete_branch_modal.vue
@@ -182,7 +182,6 @@ export default {
ref="deleteBranchButton"
:disabled="deleteButtonDisabled"
variant="danger"
- data-qa-selector="delete_branch_confirmation_button"
data-testid="delete-branch-confirmation-button"
@click="submitForm"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
index 89582e64f3a..55ff647e25f 100644
--- a/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -84,9 +84,6 @@ export default {
update(data) {
return data?.jobs?.count || 0;
},
- context: {
- isSingleRequest: true,
- },
error() {
this.error = this.$options.i18n.jobsCountErrorMsg;
},
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 d8f9eb65236..de37aa431e6 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -10,7 +10,7 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -68,7 +68,7 @@ export default {
GlPagination,
GlFormCheckbox,
TimeAgo,
- CiBadgeLink,
+ CiIcon,
JobCheckbox,
ArtifactsBulkDelete,
BulkDeleteModal,
@@ -442,7 +442,7 @@ export default {
<template #cell(job)="{ item }">
<div class="gl-display-inline-flex gl-align-items-center gl-mb-3 gl-gap-3">
<span data-testid="job-artifacts-job-status">
- <ci-badge-link :status="item.detailedStatus" size="sm" :show-text="false" />
+ <ci-icon :status="item.detailedStatus" />
</span>
<gl-link :href="item.webPath">
{{ item.name }}
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
index 85dfa12c756..fbc7ddf5c91 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -1,11 +1,13 @@
<script>
-import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import { GlButton, GlEmptyState, 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: {
+ GlButton,
+ GlEmptyState,
GlLoadingIcon,
GlTableLite,
},
@@ -37,6 +39,9 @@ export default {
},
},
computed: {
+ isMetadataMissing() {
+ return !this.components || this.components?.length === 0;
+ },
isLoading() {
return this.$apollo.queries.components.loading;
},
@@ -70,6 +75,12 @@ export default {
},
],
i18n: {
+ copyText: __('Copy value'),
+ copyAriaText: __('Copy to clipboard'),
+ emptyStateTitle: s__('CiCatalogComponent|Component details not available'),
+ emptyStateDesc: s__(
+ 'CiCatalogComponent|This tab displays auto-collected information about the components in the repository, but no information was found.',
+ ),
inputTitle: s__('CiCatalogComponent|Inputs'),
fetchError: s__("CiCatalogComponent|There was an error fetching this resource's components"),
},
@@ -79,6 +90,11 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" size="lg" />
+ <gl-empty-state
+ v-else-if="isMetadataMissing"
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDesc"
+ />
<template v-else>
<div
v-for="component in components"
@@ -88,7 +104,24 @@ export default {
>
<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-display-flex">
+ <pre
+ class="gl-w-85p gl-py-4 gl-display-flex gl-justify-content-space-between gl-m-0 gl-border-r-none"
+ ><span>{{ generateSnippet(component.path) }}</span>
+ </pre>
+ <div class="gl--flex-center gl-bg-gray-10 gl-border gl-border-l-none">
+ <gl-button
+ class="gl-p-4! gl-mr-3!"
+ category="tertiary"
+ icon="copy-to-clipboard"
+ size="small"
+ :title="$options.i18n.copyText"
+ :data-clipboard-text="generateSnippet(component.path)"
+ data-testid="copy-to-clipboard"
+ :aria-label="$options.i18n.copyAriaText"
+ />
+ </div>
+ </div>
<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">
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
index c0feb52c185..026a30988fd 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -30,12 +30,12 @@ export default {
<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-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
+ <ci-resource-components :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
index 6673785ffd2..29009c14e1b 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -2,13 +2,13 @@
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 CiIcon from '~/vue_shared/components/ci_icon.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
CiResourceAbout,
CiResourceHeaderSkeletonLoader,
GlAvatar,
@@ -102,12 +102,11 @@ export default {
{{ versionBadgeText }}
</gl-badge>
</span>
- <ci-badge-link
+ <ci-icon
v-if="hasPipelineStatus"
- class="gl-mt-2"
:status="pipelineStatus"
- size="sm"
- show-text
+ show-status-text
+ class="gl-mt-2"
/>
</div>
</div>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index 487215875c0..db84eaa82c2 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -4,12 +4,22 @@ import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
+const defaultTitle = __('CI/CD Catalog');
+const defaultDescription = s__(
+ 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
+);
+
export default {
components: {
GlBanner,
GlLink,
},
- inject: ['pageTitle', 'pageDescription'],
+ inject: {
+ pageTitle: { default: defaultTitle },
+ pageDescription: {
+ default: defaultDescription,
+ },
+ },
data() {
return {
isFeedbackBannerDismissed: localStorage.getItem(CATALOG_FEEDBACK_DISMISSED_KEY) === 'true',
@@ -50,7 +60,7 @@ export default {
</gl-banner>
<h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
<p>
- <span>{{ pageDescription }}</span>
+ <span data-testid="description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
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
index 63243539575..080955b4322 100644
--- 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
@@ -48,9 +48,6 @@ export default {
starCount() {
return this.resource?.starCount || 0;
},
- forksCount() {
- return this.resource?.forksCount || 0;
- },
hasReleasedVersion() {
return Boolean(this.latestVersion?.releasedAt);
},
@@ -111,14 +108,12 @@ export default {
<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">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between"
+ >
<span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
resource.description
}}</span>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
new file mode 100644
index 00000000000..5e8727a3ed0
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
@@ -0,0 +1,112 @@
+<script>
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
+
+export default {
+ components: {
+ CatalogHeader,
+ CatalogListSkeletonLoader,
+ CiResourcesList,
+ EmptyState,
+ },
+ data() {
+ return {
+ catalogResources: [],
+ currentPage: 1,
+ totalCount: 0,
+ pageInfo: {},
+ };
+ },
+ apollo: {
+ catalogResources: {
+ query: getCatalogResources,
+ variables() {
+ return {
+ first: ciCatalogResourcesItemsCount,
+ };
+ },
+ update(data) {
+ return data?.ciCatalogResources?.nodes || [];
+ },
+ result({ data }) {
+ const { pageInfo } = data?.ciCatalogResources || {};
+ this.pageInfo = pageInfo;
+ this.totalCount = data?.ciCatalogResources?.count || 0;
+ },
+ error(e) {
+ createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
+ },
+ },
+ },
+ computed: {
+ hasResources() {
+ return this.catalogResources.length > 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.catalogResources.loading;
+ },
+ },
+ methods: {
+ async handlePrevPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ before: this.pageInfo.startCursor,
+ last: ciCatalogResourcesItemsCount,
+ first: null,
+ },
+ });
+
+ this.currentPage -= 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ async handleNextPage() {
+ try {
+ await this.$apollo.queries.catalogResources.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+
+ this.currentPage += 1;
+ } catch (e) {
+ // Ensure that the current query is properly stoped if an error occurs.
+ this.$apollo.queries.catalogResources.stop();
+
+ createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
+ }
+ },
+ },
+ i18n: {
+ fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
+ },
+};
+</script>
+<template>
+ <div>
+ <catalog-header />
+ <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
+ <empty-state v-else-if="!hasResources" />
+ <ci-resources-list
+ v-else
+ :current-page="currentPage"
+ :page-info="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :resources="catalogResources"
+ :total-count="totalCount"
+ @onPrevPage="handlePrevPage"
+ @onNextPage="handleNextPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/global_catalog.vue b/app/assets/javascripts/ci/catalog/global_catalog.vue
new file mode 100644
index 00000000000..76eac11a122
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/global_catalog.vue
@@ -0,0 +1,10 @@
+<script>
+import CiCatalogHome from './components/ci_catalog_home.vue';
+
+export default {
+ components: { CiCatalogHome },
+};
+</script>
+<template>
+ <ci-catalog-home />
+</template>
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
index f4d1bb0eaaf..a86db4c1b03 100644
--- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -4,7 +4,6 @@ fragment CatalogResourceFields on CiCatalogResource {
name
description
starCount
- forksCount
latestVersion {
id
tagName
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
new file mode 100644
index 00000000000..aae29edef5e
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
@@ -0,0 +1,16 @@
+#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
+
+query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
+ ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
+ count
+ nodes {
+ ...CatalogResourceFields
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
new file mode 100644
index 00000000000..5815245506c
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
+
+import GlobalCatalog from './global_catalog.vue';
+import CiResourcesPage from './components/pages/ci_resources_page.vue';
+import { createRouter } from './router';
+
+export const initCatalog = (selector = '#js-ci-cd-catalog') => {
+ const el = document.querySelector(selector);
+ if (!el) {
+ return null;
+ }
+
+ const { dataset } = el;
+ const { ciCatalogPath } = dataset;
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers, cacheConfig),
+ });
+
+ return new Vue({
+ el,
+ name: 'GlobalCatalog',
+ router: createRouter(ciCatalogPath, CiResourcesPage),
+ apolloProvider,
+ provide: {
+ ciCatalogPath,
+ },
+ render(h) {
+ return h(GlobalCatalog);
+ },
+ });
+};
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 a32c5f476fb..ccfe773b01f 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
@@ -190,6 +190,11 @@ export default {
deep: true,
},
},
+ beforeMount() {
+ // reset to default environments list every time we open the drawer
+ // and re-render the environments scope dropdown
+ this.$emit('search-environment-scope', '');
+ },
mounted() {
if (this.isProtectedByDefault && !this.isEditing) {
this.variable = { ...this.variable, protected: true };
@@ -371,7 +376,6 @@ export default {
: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"
@@ -388,7 +392,6 @@ export default {
rows="3"
max-rows="10"
data-testid="ci-variable-value"
- data-qa-selector="ci_variable_value_field"
spellcheck="false"
/>
<p
@@ -419,15 +422,14 @@ export default {
variant="danger"
category="secondary"
class="gl-mr-3"
- data-testid="ci-variable-delete-btn"
+ data-testid="ci-variable-delete-button"
>{{ $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"
+ data-testid="ci-variable-confirm-button"
@click="submit"
>{{ modalActionText }}
</gl-button>
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
deleted file mode 100644
index cc664d76267..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
+++ /dev/null
@@ -1,511 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
-} from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { getCookie, setCookie } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-import {
- allEnvironments,
- AWS_TOKEN_CONSTANTS,
- ADD_CI_VARIABLE_MODAL_ID,
- AWS_TIP_DISMISSED_COOKIE_NAME,
- AWS_TIP_TITLE,
- AWS_TIP_MESSAGE,
- CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- defaultVariableState,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- EVENT_LABEL,
- EVENT_ACTION,
- EXPANDED_VARIABLES_NOTE,
- EDIT_VARIABLE_ACTION,
- FLAG_LINK_TITLE,
- VARIABLE_ACTIONS,
- variableOptions,
-} from '../constants';
-import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
-import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
-
-const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
-
-export default {
- components: {
- CiEnvironmentsDropdown,
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
- },
- mixins: [glFeatureFlagsMixin(), trackingMixin],
- inject: [
- 'containsVariableReferenceLink',
- 'environmentScopeLink',
- 'isProtectedByDefault',
- 'maskedEnvironmentVariablesLink',
- 'maskableRawRegex',
- 'maskableRegex',
- ],
- props: {
- areEnvironmentsLoading: {
- type: Boolean,
- required: true,
- },
- areScopedVariablesAvailable: {
- type: Boolean,
- required: false,
- default: false,
- },
- environments: {
- type: Array,
- required: false,
- default: () => [],
- },
- hideEnvironmentScope: {
- type: Boolean,
- required: false,
- default: false,
- },
- mode: {
- type: String,
- required: true,
- validator(val) {
- return VARIABLE_ACTIONS.includes(val);
- },
- },
- selectedVariable: {
- type: Object,
- required: false,
- default: () => {},
- },
- variables: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- newEnvironments: [],
- isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
- validationErrorEventProperty: '',
- variable: { ...defaultVariableState, ...this.selectedVariable },
- };
- },
- computed: {
- canMask() {
- const regex = RegExp(this.useRawMaskableRegexp ? this.maskableRawRegex : this.maskableRegex);
- return regex.test(this.variable.value);
- },
- canSubmit() {
- return this.variableValidationState && this.variable.key !== '';
- },
- containsVariableReference() {
- const regex = /\$/;
- return regex.test(this.variable.value) && this.isExpanded;
- },
- displayMaskedError() {
- return !this.canMask && this.variable.masked;
- },
- isEditing() {
- return this.mode === EDIT_VARIABLE_ACTION;
- },
- isExpanded() {
- return !this.isRaw;
- },
- isRaw() {
- return this.variable.raw;
- },
- isTipVisible() {
- return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
- },
- maskedFeedback() {
- return this.displayMaskedError
- ? __('This variable value does not meet the masking requirements.')
- : '';
- },
- maskedState() {
- if (this.displayMaskedError) {
- return false;
- }
- return true;
- },
- modalActionText() {
- return this.isEditing ? __('Update variable') : __('Add variable');
- },
- tokenValidationFeedback() {
- const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
- if (!this.tokenValidationState && tokenSpecificFeedback) {
- return tokenSpecificFeedback;
- }
- return '';
- },
- tokenValidationState() {
- const validator = this.$options.tokens?.[this.variable.key]?.validation;
-
- if (validator) {
- return validator(this.variable.value);
- }
-
- return true;
- },
- useRawMaskableRegexp() {
- return this.isRaw;
- },
- variableValidationFeedback() {
- return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
- },
- variableValidationState() {
- return this.variable.value === '' || (this.tokenValidationState && this.maskedState);
- },
- variableValueHelpText() {
- return this.variable.masked
- ? __('Value must meet regular expression requirements to be masked.')
- : '';
- },
- },
- watch: {
- variable: {
- handler() {
- this.trackVariableValidationErrors();
- },
- deep: true,
- },
- },
- methods: {
- addVariable() {
- this.$emit('add-variable', this.variable);
- },
- deleteVariable() {
- this.$emit('delete-variable', this.variable);
- },
- updateVariable() {
- this.$emit('update-variable', this.variable);
- },
- dismissTip() {
- setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
- this.isTipDismissed = true;
- },
- deleteVarAndClose() {
- this.deleteVariable();
- this.hideModal();
- },
- hideModal() {
- this.$refs.modal.hide();
- },
- onShow() {
- this.setVariableProtectedByDefault();
- },
- resetModalHandler() {
- this.resetVariableData();
- this.resetValidationErrorEvents();
-
- this.$emit('close-form');
- },
- resetVariableData() {
- this.variable = { ...defaultVariableState };
- },
- setEnvironmentScope(scope) {
- this.variable = { ...this.variable, environmentScope: scope };
- },
- setVariableRaw(expanded) {
- this.variable = { ...this.variable, raw: !expanded };
- },
- setVariableProtected() {
- this.variable = { ...this.variable, protected: true };
- },
- updateOrAddVariable() {
- if (this.isEditing) {
- this.updateVariable();
- } else {
- this.addVariable();
- }
- this.hideModal();
- },
- setVariableProtectedByDefault() {
- if (this.isProtectedByDefault && !this.isEditing) {
- this.setVariableProtected();
- }
- },
- trackVariableValidationErrors() {
- const property = this.getTrackingErrorProperty();
- if (!this.validationErrorEventProperty && property) {
- this.track(EVENT_ACTION, { property });
- this.validationErrorEventProperty = property;
- }
- },
- getTrackingErrorProperty() {
- let property;
- if (this.variable.value?.length && !property) {
- if (this.displayMaskedError && this.maskableRegex?.length) {
- const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
- const regex = new RegExp(supportedChars, 'g');
- property = this.variable.value.replace(regex, '');
- }
- if (this.containsVariableReference) {
- property = '$';
- }
- }
-
- return property;
- },
- resetValidationErrorEvents() {
- this.validationErrorEventProperty = '';
- },
- },
- i18n: {
- awsTipTitle: AWS_TIP_TITLE,
- awsTipMessage: AWS_TIP_MESSAGE,
- containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- defaultScope: allEnvironments.text,
- environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
- expandedVariablesNote: EXPANDED_VARIABLES_NOTE,
- flagsLinkTitle: FLAG_LINK_TITLE,
- },
- flagLink: helpPagePath('ci/variables/index', {
- anchor: 'define-a-cicd-variable-in-the-ui',
- }),
- oidcLink: helpPagePath('ci/cloud_services/index', {
- anchor: 'oidc-authorization-with-your-cloud-provider',
- }),
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- tokens: awsTokens,
- tokenList: awsTokenList,
- variableOptions,
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- :modal-id="$options.modalId"
- :title="modalActionText"
- static
- lazy
- @hidden="resetModalHandler"
- @shown="onShow"
- >
- <gl-collapse :visible="isTipVisible">
- <gl-alert
- :title="$options.i18n.awsTipTitle"
- variant="warning"
- class="gl-mb-5"
- data-testid="aws-guidance-tip"
- @dismiss="dismissTip"
- >
- <gl-sprintf :message="$options.i18n.awsTipMessage">
- <template #link="{ content }">
- <gl-link :href="$options.oidcLink">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- </gl-collapse>
- <form>
- <gl-form-combobox
- v-model="variable.key"
- :token-list="$options.tokenList"
- :label-text="__('Key')"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
-
- <gl-form-group
- :label="__('Value')"
- label-for="ci-variable-value"
- :state="variableValidationState"
- :description="variableValueHelpText"
- :invalid-feedback="variableValidationFeedback"
- >
- <gl-form-textarea
- id="ci-variable-value"
- ref="valueField"
- v-model="variable.value"
- :state="variableValidationState"
- rows="3"
- max-rows="10"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- class="gl-font-monospace!"
- spellcheck="false"
- />
- <p v-if="isRaw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
- {{ __('Variable value will be evaluated as raw string.') }}
- </p>
- </gl-form-group>
-
- <div class="gl-display-flex">
- <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="gl-w-half gl-mr-5">
- <gl-form-select
- id="ci-variable-type"
- v-model="variable.variableType"
- :options="$options.variableOptions"
- />
- </gl-form-group>
-
- <template v-if="!hideEnvironmentScope">
- <gl-form-group
- label-for="ci-variable-env"
- class="gl-w-half"
- data-testid="environment-scope"
- >
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ __('Environment scope') }}
- </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"
- :are-environments-loading="areEnvironmentsLoading"
- :selected-environment-scope="variable.environmentScope"
- :environments="environments"
- @select-environment="setEnvironmentScope"
- @search-environment-scope="$emit('search-environment-scope', $event)"
- />
-
- <gl-form-input v-else :value="$options.i18n.defaultScope" class="gl-w-full" readonly />
- </gl-form-group>
- </template>
- </div>
-
- <gl-form-group>
- <template #label>
- <div class="gl-display-flex gl-align-items-center">
- <span class="gl-mr-2">
- {{ __('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"
- class="gl-mb-0"
- data-testid="ci-variable-protected-checkbox"
- :data-is-protected-checked="variable.protected"
- >
- {{ __('Protect variable') }}
- <p class="gl-mt-2 text-secondary">
- {{ __('Export variable to pipelines running on protected branches and tags only.') }}
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox
- ref="masked-ci-variable"
- v-model="variable.masked"
- data-testid="ci-variable-masked-checkbox"
- >
- {{ __('Mask variable') }}
- <p class="gl-mt-2 text-secondary">
- <gl-sprintf
- :message="
- __(
- 'Mask this variable in job logs if it meets %{linkStart}regular expression requirements%{linkEnd}.',
- )
- "
- >
- <template #link="{ content }"
- ><gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </gl-form-checkbox>
- <gl-form-checkbox
- ref="expanded-ci-variable"
- :checked="isExpanded"
- data-testid="ci-variable-expanded-checkbox"
- @change="setVariableRaw"
- >
- {{ __('Expand variable reference') }}
- <p class="gl-mt-2 gl-mb-0 gl-text-secondary">
- <gl-sprintf :message="$options.i18n.expandedVariablesNote">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- </gl-form-checkbox>
- </gl-form-group>
- </form>
-
- <gl-alert
- v-if="containsVariableReference"
- :title="__('Value might contain a variable reference')"
- :dismissible="false"
- variant="warning"
- data-testid="contains-variable-reference"
- >
- <gl-sprintf :message="$options.i18n.containsVariableReferenceMessage">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #docsLink="{ content }">
- <gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <template #modal-footer>
- <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
- <gl-button
- v-if="isEditing"
- ref="deleteCiVariable"
- variant="danger"
- category="secondary"
- @click="deleteVarAndClose"
- >{{ __('Delete variable') }}</gl-button
- >
- <gl-button
- ref="updateOrAddVariable"
- :disabled="!canSubmit"
- variant="confirm"
- category="primary"
- data-testid="ciUpdateOrAddVariableBtn"
- data-qa-selector="ci_variable_save_button"
- @click="updateOrAddVariable"
- >{{ modalActionText }}
- </gl-button>
- </template>
- </gl-modal>
-</template>
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 f2d81b3f271..99270d36df7 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
@@ -3,13 +3,11 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
import CiVariableDrawer from './ci_variable_drawer.vue';
import CiVariableTable from './ci_variable_table.vue';
-import CiVariableModal from './ci_variable_modal.vue';
export default {
components: {
CiVariableDrawer,
CiVariableTable,
- CiVariableModal,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -65,15 +63,6 @@ export default {
showForm() {
return VARIABLE_ACTIONS.includes(this.mode);
},
- useDrawerForm() {
- return this.glFeatures?.ciVariableDrawer;
- },
- showDrawer() {
- return this.showForm && this.useDrawerForm;
- },
- showModal() {
- return this.showForm && !this.useDrawerForm;
- },
},
methods: {
addVariable(variable) {
@@ -116,23 +105,8 @@ export default {
@delete-variable="deleteVariable"
@sort-changed="(val) => $emit('sort-changed', val)"
/>
- <ci-variable-modal
- v-if="showModal"
- :are-environments-loading="areEnvironmentsLoading"
- :are-scoped-variables-available="areScopedVariablesAvailable"
- :environments="environments"
- :hide-environment-scope="hideEnvironmentScope"
- :variables="variables"
- :mode="mode"
- :selected-variable="selectedVariable"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @close-form="closeForm"
- @update-variable="updateVariable"
- @search-environment-scope="$emit('search-environment-scope', $event)"
- />
<ci-variable-drawer
- v-if="showDrawer"
+ v-if="showForm"
:are-environments-loading="areEnvironmentsLoading"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index 3d62313815c..86287d586ec 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
@@ -16,7 +16,6 @@ import {
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- ADD_CI_VARIABLE_MODAL_ID,
DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
EXCEEDS_VARIABLE_LIMIT_TEXT,
MAXIMUM_VARIABLE_LIMIT_REACHED,
@@ -25,7 +24,6 @@ import {
import { convertEnvironmentScope } from '../utils';
export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
defaultFields: [
{
key: 'key',
@@ -243,10 +241,8 @@ export default {
>{{ valuesButtonText }}</gl-button
>
<gl-button
- v-gl-modal-directive="$options.modalId"
size="small"
:disabled="exceedsVariableLimit"
- data-qa-selector="add_ci_variable_button"
data-testid="add-ci-variable-button"
@click="setSelectedVariable()"
>{{ $options.i18n.addButton }}</gl-button
@@ -375,12 +371,11 @@ export default {
<template v-if="!isInheritedGroupVars" #cell(actions)="{ item }">
<div class="gl-display-flex gl-justify-content-end gl-mt-n2 gl-mb-n2">
<gl-button
- v-gl-modal-directive="$options.modalId"
icon="pencil"
size="small"
class="gl-mr-3"
:aria-label="$options.i18n.editButton"
- data-qa-selector="edit_ci_variable_button"
+ data-testid="edit-ci-variable-button"
@click="setSelectedVariable(item.index)"
/>
<gl-button
@@ -390,7 +385,6 @@ export default {
icon="remove"
size="small"
:aria-label="$options.i18n.deleteButton"
- data-qa-selector="delete_ci_variable_button"
/>
<gl-modal
ref="modal"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index fc37b62299d..d85827b8220 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -1,6 +1,5 @@
import { __, s__, sprintf } from '~/locale';
-export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
export const ENVIRONMENT_QUERY_LIMIT = 30;
export const SORT_DIRECTIONS = {
@@ -45,7 +44,6 @@ export const AWS_TIP_MESSAGE = s__(
'CiVariable|GitLab CI/CD supports OpenID Connect (OIDC) to give your build and deployment jobs access to cloud credentials and services. %{linkStart}How do I configure OIDC for my cloud provider?%{linkEnd}',
);
-export const EVENT_LABEL = 'ci_variable_modal';
export const DRAWER_EVENT_LABEL = 'ci_variable_drawer';
export const EVENT_ACTION = 'validation_error';
diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 13b5120654a..d63d2d1713e 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -13,8 +13,6 @@ import PipelineUrl from '../pipelines_page/components/pipeline_url.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
@@ -77,7 +75,6 @@ export default {
{
key: 'status',
label: s__('Pipeline|Status'),
- thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-15p',
tdClass: this.tdClasses,
thAttr: { 'data-testid': 'status-th' },
@@ -85,7 +82,6 @@ export default {
{
key: 'pipeline',
label: __('Pipeline'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses}`,
columnClass: 'gl-w-30p',
thAttr: { 'data-testid': 'pipeline-th' },
@@ -93,7 +89,6 @@ export default {
{
key: 'triggerer',
label: s__('Pipeline|Created by'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'triggerer-th' },
@@ -101,14 +96,12 @@ export default {
{
key: 'stages',
label: s__('Pipeline|Stages'),
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-quarter',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'actions',
- thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
@@ -137,8 +130,7 @@ export default {
return cleanLeadingSeparator(item.project.full_path);
},
failedJobsCount(pipeline) {
- // Remove `pipeline?.failed_builds?.length` when we remove `ci_fix_performance_pipelines_json_endpoint`.
- return pipeline?.failed_builds_count || pipeline?.failed_builds?.length || 0;
+ return pipeline?.failed_builds_count || 0;
},
onRefreshPipelinesTable() {
this.$emit('refresh-pipelines-table');
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 b0fa724d450..c266e061513 100644
--- a/app/assets/javascripts/ci/common/private/job_action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -119,6 +119,7 @@ export default {
ref="button"
:class="cssClass"
:disabled="isDisabled"
+ size="small"
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-button"
@click.stop="onClickAction"
@@ -129,8 +130,17 @@ export default {
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full"
data-testid="ci-action-icon-tooltip-wrapper"
>
- <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" />
- <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
+ <gl-loading-icon
+ v-if="isLoading"
+ size="sm"
+ class="gl-button-icon gl-m-2 js-action-icon-loading"
+ />
+ <gl-icon
+ v-else
+ :name="actionIcon"
+ class="gl-button-icon gl-p-1 gl-mr-0!"
+ :aria-label="actionIcon"
+ />
</div>
</gl-button>
</template>
diff --git a/app/assets/javascripts/ci/common/private/job_links_layer.vue b/app/assets/javascripts/ci/common/private/job_links_layer.vue
index 59260ca3f81..9b3647e9c55 100644
--- a/app/assets/javascripts/ci/common/private/job_links_layer.vue
+++ b/app/assets/javascripts/ci/common/private/job_links_layer.vue
@@ -1,5 +1,6 @@
<script>
import { memoize } from 'lodash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '~/ci/utils';
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
import LinksInner from '~/ci/pipeline_details/graph/components/links_inner.vue';
@@ -16,6 +17,7 @@ export default {
components: {
LinksInner,
},
+ mixins: [glFeatureFlagMixin()],
props: {
containerMeasurements: {
type: Object,
@@ -50,6 +52,9 @@ export default {
showLinkedLayers() {
return this.showLinks && !this.containerZero;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
@@ -68,7 +73,10 @@ export default {
<slot></slot>
</links-inner>
<div v-else>
- <div class="gl-display-flex gl-relative">
+ <div
+ class="gl-display-flex gl-relative"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph }"
+ >
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/ci/common/private/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index 1c7f5a7476d..b4e831d69d4 100644
--- a/app/assets/javascripts/ci/common/private/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
@@ -30,7 +30,7 @@ export default {
</script>
<template>
<span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
+ <ci-icon :size="iconSize" :status="status" :show-tooltip="false" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
index 5b60528f521..138a44a8dd0 100644
--- a/app/assets/javascripts/ci/constants.js
+++ b/app/assets/javascripts/ci/constants.js
@@ -37,4 +37,5 @@ export const TRACKING_CATEGORIES = {
search: 'pipelines_filtered_search',
failed: 'pipeline_failed_jobs_tab',
tests: 'pipeline_tests_tab',
+ listbox: 'pipeline_id_iid_listbox',
};
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 00d15f87064..1aa83a94bc5 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,12 +4,12 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
TimeagoTooltip,
GlButton,
GlAvatarLink,
@@ -113,7 +113,7 @@ export default {
</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" />
+ <ci-icon class="gl-mr-3" :status="status" show-status-text />
<template v-if="shouldRenderTriggeredLabel">{{ __('Started') }}</template>
<template v-else>{{ __('Created') }}</template>
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 658a94e6af4..d36701323da 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
@@ -17,7 +17,8 @@ export default {
},
isClosed: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
path: {
type: String,
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 8e87f118fa4..4ec9044a21c 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -63,11 +63,11 @@ export default {
<gl-icon
v-if="isActive"
name="arrow-right"
+ :show-tooltip="false"
class="icon-arrow-right gl-absolute gl-display-block"
- :size="14"
/>
- <ci-icon :status="job.status" class="gl-mr-3" :size="14" />
+ <ci-icon :status="job.status" :show-tooltip="false" class="gl-mr-3" />
<span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index 7744395734f..e229abcbe12 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -14,7 +14,7 @@ export default {
GlDisclosureDropdown,
GlLink,
GlSprintf,
- CiBadgeLink,
+ CiIcon,
},
props: {
pipeline: {
@@ -94,7 +94,10 @@ export default {
</script>
<template>
<div class="dropdown">
- <div class="gl-display-flex gl-flex-wrap gl-gap-2 js-pipeline-info" data-testid="pipeline-info">
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-gap-2 js-pipeline-info"
+ data-testid="pipeline-info"
+ >
<gl-sprintf :message="pipelineInfo">
<template #bold="{ content }">
<span class="gl-display-flex gl-font-weight-bold">{{ content }}</span>
@@ -108,9 +111,9 @@ export default {
>
</template>
<template #status>
- <ci-badge-link
+ <ci-icon
:status="pipeline.details.status"
- size="sm"
+ show-status-text
data-testid="pipeline-status-link"
/>
</template>
@@ -125,7 +128,7 @@ export default {
<template #ref>
<gl-link
:href="pipeline.ref.path"
- class="link-commit ref-name gl-mt-1"
+ class="link-commit ref-name"
data-testid="source-ref-link"
>{{ pipeline.ref.name }}</gl-link
><clipboard-button
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 119f8259be7..e0708289b43 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -307,7 +307,7 @@ export default {
@scrollJobLogBottom="scrollBottom"
@searchResults="setSearchResults"
/>
- <log :job-log="jobLog" :is-complete="isJobLogComplete" :search-results="searchResults" />
+ <log :search-results="searchResults" />
</div>
<!-- EO job log -->
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index fa23589f7d6..6f538e3b3d4 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -175,7 +175,7 @@ export const fetchJobLog = ({ dispatch, state }) =>
}
})
.catch((e) => {
- if (e.response.status === HTTP_STATUS_FORBIDDEN) {
+ if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
dispatch('receiveJobLogUnauthorizedError');
} else {
reportToSentry('job_actions', e);
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index b18a3fa162d..c8b33638821 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -117,28 +117,31 @@ export const getNextLineNumber = (acc) => {
* @returns Array parsed log lines
*/
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));
- }
+ 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;
- }, prevLogLines);
+ return acc;
+ },
+ [...prevLogLines],
+ );
/**
* Finds the repeated offset, removes the old one
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 fbdfc7c9c6a..b97243cf2ca 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
@@ -136,8 +136,8 @@ export default {
v-if="triggered"
variant="info"
:size="$options.badgeSize"
- data-testid="triggered-job-badge"
- >{{ s__('Job|triggered') }}
+ data-testid="trigger-token-job-badge"
+ >{{ s__('Job|trigger token') }}
</gl-badge>
<gl-badge
v-if="showAllowedToFailBadge"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index a2b6a430138..efa74d86bd6 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,14 +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 CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
components: {
- CiBadgeLink,
+ CiIcon,
GlIcon,
TimeAgoTooltip,
},
@@ -38,7 +38,7 @@ export default {
<template>
<div>
- <ci-badge-link :status="job.detailedStatus" />
+ <ci-icon :status="job.detailedStatus" show-status-text />
<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" />
diff --git a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
index 03e0f2dadc8..09bbb7afbca 100644
--- a/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
+++ b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
@@ -58,9 +58,6 @@ export default {
},
jobsCount: {
query: GetJobsCount,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index 70b758ae6b0..51d0e980e78 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -2,7 +2,6 @@ import { __, s__ } from '~/locale';
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
-export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
index f098d790736..3da2f27c1b9 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
@@ -4,6 +4,7 @@ import {
generateColumnsFromLayersListMemoized,
keepLatestDownstreamPipelines,
} from '~/ci/pipeline_details/utils/parsing_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LinksLayer from '../../../common/private/job_links_layer.vue';
import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from '../constants';
import { validateConfigPaths } from '../utils';
@@ -19,6 +20,7 @@ export default {
LinkedPipelinesColumn,
StageColumnComponent,
},
+ mixins: [glFeatureFlagMixin()],
props: {
configPaths: {
type: Object,
@@ -132,6 +134,9 @@ export default {
upstreamPipelines() {
return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
@@ -178,10 +183,15 @@ export default {
<div class="js-pipeline-graph">
<div
ref="mainPipelineContainer"
- class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ class="pipeline-graph gl-display-flex gl-position-relative gl-white-space-nowrap gl-rounded-lg"
:class="{
- 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
+ 'gl-bg-gray-10': !isNewPipelineGraph,
+ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isNewPipelineGraph && !isLinkedPipeline,
+ 'pipeline-graph-container gl-bg-gray-10 gl-pipeline-min-h gl-align-items-flex-start gl-pt-3 gl-pb-8 gl-mt-3 gl-overflow-auto':
+ isNewPipelineGraph && !isLinkedPipeline,
+ 'gl-bg-gray-50 gl-sm-ml-5': isNewPipelineGraph && isLinkedPipeline,
}"
+ data-testid="pipeline-container"
>
<linked-graph-wrapper>
<template #upstream>
@@ -199,7 +209,7 @@ export default {
/>
</template>
<template #main>
- <div :id="containerId" :ref="containerId">
+ <div :id="containerId" :ref="containerId" class="pipeline-links-container">
<links-layer
:pipeline-data="layout"
:pipeline-id="pipeline.id"
@@ -238,7 +248,7 @@ export default {
<template #downstream>
<linked-pipelines-column
v-if="showDownstreamPipelines"
- class="gl-mr-6"
+ :class="{ 'gl-sm-ml-3': isNewPipelineGraph }"
:config-paths="configPaths"
:linked-pipelines="downstreamPipelines"
:column-title="__('Downstream')"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
index fb7dcb300f1..114b224fbe7 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
@@ -1,11 +1,11 @@
<script>
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__ } from '~/locale';
import { STAGE_VIEW, LAYER_VIEW } from '../constants';
export default {
name: 'GraphViewSelector',
-
components: {
GlAlert,
GlButton,
@@ -13,7 +13,7 @@ export default {
GlLoadingIcon,
GlToggle,
},
-
+ mixins: [glFeatureFlagMixin()],
props: {
showLinks: {
type: Boolean,
@@ -77,6 +77,9 @@ export default {
};
});
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
watch: {
/*
@@ -138,7 +141,13 @@ export default {
<template>
<div>
- <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
+ <div
+ class="gl-relative gl-display-flex gl-align-items-center gl-my-4"
+ :class="{
+ 'gl-w-max-content': !isNewPipelineGraph,
+ 'gl-flex-wrap gl-sm-flex-nowrap': isNewPipelineGraph,
+ }"
+ >
<gl-loading-icon
v-if="isSwitcherLoading"
data-testid="switcher-loading-state"
@@ -161,7 +170,10 @@ export default {
<gl-toggle
v-model="showLinksActive"
data-testid="show-links-toggle"
- class="gl-mx-4"
+ :class="{
+ 'gl-mx-4': !isNewPipelineGraph,
+ 'gl-sm-ml-4 gl-mt-4 gl-sm-mt-0': isNewPipelineGraph,
+ }"
:label="$options.i18n.linksLabelText"
:is-loading="isToggleLoading"
label-position="left"
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 bb36ac8b6ab..c6340e6787a 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.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,
- CiBadgeLink,
+ CiIcon,
GlBadge,
GlForm,
GlFormCheckbox,
@@ -329,7 +329,7 @@ export default {
@mouseout="hideTooltips"
>
<div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-badge-link :status="job.status" size="md" :show-text="false" :use-link="false" />
+ <ci-icon :status="job.status" :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
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
index fb2280d971a..0d72373a0f5 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
@@ -1,5 +1,20 @@
+<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+export default {
+ mixins: [glFeatureFlagMixin()],
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
+};
+</script>
<template>
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex"
+ :class="{ 'gl-flex-wrap gl-sm-flex-nowrap gl-w-full': isNewPipelineGraph }"
+ >
<slot name="upstream"></slot>
<slot name="main"></slot>
<slot name="downstream"></slot>
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 5960eea5b4f..26521f87426 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
@@ -7,13 +7,14 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -22,13 +23,14 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- CiBadgeLink,
+ CiIcon,
GlBadge,
GlButton,
GlLink,
GlLoadingIcon,
GlTooltip,
},
+ mixins: [glFeatureFlagMixin()],
styles: {
actionSizeClasses: ['gl-h-7 gl-w-7'],
flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
@@ -115,9 +117,6 @@ export default {
downstreamTitle() {
return this.childPipeline ? this.sourceJobName : this.pipeline.project.name;
},
- flexDirection() {
- return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
- },
graphqlPipelineId() {
return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id);
},
@@ -176,6 +175,9 @@ export default {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
@@ -231,9 +233,15 @@ export default {
<template>
<div
ref="linkedPipeline"
- class="gl-h-full gl-display-flex! gl-px-2"
- :class="flexDirection"
+ class="linked-pipeline-container gl-h-full gl-display-flex!"
+ :class="{
+ 'gl-flex-direction-row-reverse': isUpstream,
+ 'gl-flex-direction-row': !isUpstream,
+ 'gl-px-2': !isNewPipelineGraph,
+ 'gl-w-full gl-sm-w-auto': isNewPipelineGraph,
+ }"
data-testid="linked-pipeline-container"
+ :aria-expanded="expanded"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
@@ -242,17 +250,15 @@ 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-badge-link
+ <ci-icon
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"
+ class="gl-display-flex gl-flex-direction-column gl-line-height-normal gl-downstream-pipeline-job-width"
>
<span class="gl-text-truncate" data-testid="downstream-title-content">
{{ downstreamTitle }}
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
index 2de7e43c9b1..395770826d8 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import { reportToSentry } from '~/ci/utils';
import { LOAD_FAILURE } from '../../constants';
@@ -18,6 +19,7 @@ export default {
LinkedPipeline,
PipelineGraph: () => import('./graph_component.vue'),
},
+ mixins: [glFeatureFlagMixin()],
props: {
columnTitle: {
type: String,
@@ -63,23 +65,30 @@ export default {
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
- 'gl-pl-3',
- 'gl-mb-5',
],
minWidth: `${ONE_COL_WIDTH}px`,
computed: {
columnClass() {
- const positionValues = {
+ const positionValuesOld = {
right: 'gl-ml-6',
left: 'gl-mx-6',
};
+ const positionValues = {
+ right: 'gl-mx-5',
+ left: 'gl-mx-4 gl-flex-basis-full',
+ };
+ const usePositionValues = this.isNewPipelineGraph ? positionValues : positionValuesOld;
- return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
+ return `graph-position-${this.graphPosition} ${usePositionValues[this.graphPosition]}`;
},
computedTitleClasses() {
const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
- return [...this.$options.titleClasses, ...positionalClasses];
+ return [
+ ...this.$options.titleClasses,
+ !this.isNewPipelineGraph ?? ['gl-pl-3', 'gl-mb-5'],
+ ...positionalClasses,
+ ];
},
graphPosition() {
return this.isUpstream ? 'left' : 'right';
@@ -93,6 +102,9 @@ export default {
minWidth() {
return this.isUpstream ? 0 : this.$options.minWidth;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
},
methods: {
getPipelineData(pipeline) {
@@ -197,7 +209,7 @@ export default {
</script>
<template>
- <div class="gl-display-flex">
+ <div class="gl-display-flex" :class="{ 'gl-w-full gl-sm-w-auto': isNewPipelineGraph }">
<div :class="columnClass" class="linked-pipelines-column">
<div data-testid="linked-column-title" :class="computedTitleClasses">
{{ columnTitle }}
@@ -206,8 +218,12 @@ export default {
<li
v-for="pipeline in linkedPipelines"
:key="pipeline.id"
- class="gl-display-flex gl-mb-3"
- :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
+ class="gl-display-flex"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-flex-wrap gl-sm-flex-nowrap gl-mb-6': isNewPipelineGraph,
+ 'gl-flex-direction-row-reverse': !isNewPipelineGraph && isUpstream,
+ }"
>
<linked-pipeline
class="gl-display-inline-block"
@@ -224,12 +240,15 @@ export default {
<div
v-if="showContainer(pipeline.id)"
:style="{ minWidth }"
- class="gl-display-inline-block"
+ class="gl-display-inline-block pipeline-show-container"
>
<pipeline-graph
v-if="isExpanded(pipeline.id)"
:type="type"
- class="gl-inline-block gl-mt-n2"
+ class="gl-inline-block"
+ :class="{
+ 'gl-mt-n2': !isNewPipelineGraph,
+ }"
:config-paths="configPaths"
:pipeline="currentPipeline"
:computed-pipeline-info="getPipelineLayers(pipeline.id)"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
index bcd7705669e..7c07591d0de 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
@@ -1,5 +1,12 @@
<script>
+import { GlCard } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
export default {
+ components: {
+ GlCard,
+ },
+ mixins: [glFeatureFlagMixin()],
props: {
stageClasses: {
type: String,
@@ -12,18 +19,37 @@ export default {
default: '',
},
},
+ computed: {
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ },
};
</script>
<template>
<div>
- <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses">
- <slot name="stages"> </slot>
- </div>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
- :class="jobClasses"
+ <gl-card
+ v-if="isNewPipelineGraph"
+ class="gl-rounded-lg"
+ header-class="gl-rounded-lg gl-px-0 gl-py-0 gl-bg-white gl-border-b-0"
+ body-class="gl-pt-2 gl-pb-0 gl-px-2"
>
- <slot name="jobs"> </slot>
- </div>
+ <template #header>
+ <slot name="stages"></slot>
+ </template>
+
+ <slot name="jobs"></slot>
+ </gl-card>
+ <template v-else>
+ <div class="gl-display-flex gl-align-items-center gl-w-full" :class="stageClasses">
+ <slot name="stages"> </slot>
+ </div>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
+ :class="jobClasses"
+ >
+ <slot name="jobs"> </slot>
+ </div>
+ </template>
</div>
</template>
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 6030adc96ad..01a9c6d030d 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
@@ -68,7 +68,7 @@ export default {
required: true,
},
},
- jobClasses: [
+ legacyJobClasses: [
'gl-p-3',
'gl-border-gray-100',
'gl-border-solid',
@@ -82,18 +82,43 @@ export default {
'gl-hover-border-gray-200',
'gl-focus-border-gray-200',
],
- titleClasses: [
+ jobClasses: [
+ 'gl-p-3',
+ 'gl-border-0',
+ 'gl-bg-transparent',
+ 'gl-rounded-base',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ ],
+ legacyTitleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
'gl-text-truncate',
'gl-line-height-36',
'gl-pl-3',
],
+ titleClasses: [
+ 'gl-font-weight-bold',
+ 'gl-pipeline-job-width',
+ 'gl-text-truncate',
+ 'gl-line-height-36',
+ 'gl-pl-4',
+ 'gl-mb-n2',
+ ],
computed: {
canUpdatePipeline() {
return this.userPermissions.updatePipeline;
},
columnSpacingClass() {
+ if (this.isNewPipelineGraph) {
+ const baseClasses = 'stage-column gl-relative gl-flex-basis-full';
+ return this.isStageView
+ ? `${baseClasses} is-stage-view gl-m-5`
+ : `${baseClasses} gl-my-5 gl-mx-7`;
+ }
+
return this.isStageView ? 'gl-px-6' : 'gl-px-9';
},
hasAction() {
@@ -102,6 +127,17 @@ export default {
showStageName() {
return !this.isStageView;
},
+ isNewPipelineGraph() {
+ return this.glFeatures.newPipelineGraph;
+ },
+ jobClasses() {
+ return this.isNewPipelineGraph ? this.$options.jobClasses : this.$options.legacyJobClasses;
+ },
+ titleClasses() {
+ return this.isNewPipelineGraph
+ ? this.$options.titleClasses
+ : this.$options.legacyTitleClasses;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
@@ -135,12 +171,16 @@ export default {
};
</script>
<template>
- <root-graph-layout :class="columnSpacingClass" data-testid="stage-column">
+ <root-graph-layout
+ :class="columnSpacingClass"
+ class="stage-column gl-relative gl-flex-basis-full"
+ data-testid="stage-column"
+ >
<template #stages>
<div
data-testid="stage-column-title"
- class="gl-display-flex gl-justify-content-space-between gl-relative"
- :class="$options.titleClasses"
+ class="stage-column-title gl-display-flex gl-justify-content-space-between gl-relative"
+ :class="titleClasses"
>
<span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p">
{{ name }}
@@ -161,7 +201,11 @@ export default {
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
- class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
+ class="gl-relative gl-white-space-normal gl-pipeline-job-width"
+ :class="{
+ 'gl-mb-3': !isNewPipelineGraph,
+ 'gl-mb-2': isNewPipelineGraph,
+ }"
@mouseenter="$emit('jobHover', group.name)"
@mouseleave="$emit('jobHover', '')"
>
@@ -174,7 +218,7 @@ export default {
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
:class="[
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
@@ -188,7 +232,7 @@ export default {
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
- :css-class-job-name="$options.jobClasses"
+ :css-class-job-name="jobClasses"
/>
</div>
</div>
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 51a68f6619a..651662d6395 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
@@ -17,7 +17,7 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-
import { __, s__, sprintf, formatNumber } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
@@ -38,7 +38,7 @@ export default {
pipelineRetry: 'pipelineRetry',
finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'],
components: {
- CiBadgeLink,
+ CiIcon,
ClipboardButton,
GlAlert,
GlBadge,
@@ -58,13 +58,17 @@ export default {
i18n: {
scheduleBadgeText: s__('Pipelines|Scheduled'),
scheduleBadgeTooltip: __('This pipeline was created by a schedule'),
+ triggerBadgeText: __('trigger token'),
+ triggerBadgeTooltip: __(
+ 'This pipeline was created by an API call authenticated with a trigger token',
+ ),
childBadgeText: s__('Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})'),
childBadgeTooltip: __('This is a child pipeline within the parent pipeline'),
latestBadgeText: s__('Pipelines|latest'),
latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'),
mergeTrainBadgeText: s__('Pipelines|merge train'),
mergeTrainBadgeTooltip: s__(
- 'Pipelines|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.',
+ 'Pipelines|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.',
),
invalidBadgeText: s__('Pipelines|yaml invalid'),
failedBadgeText: s__('Pipelines|error'),
@@ -74,7 +78,11 @@ export default {
),
detachedBadgeText: s__('Pipelines|merge request'),
detachedBadgeTooltip: s__(
- "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.",
+ "Pipelines|This pipeline ran on the contents of the merge request's source branch, not the target branch.",
+ ),
+ mergedResultsBadgeText: s__('Pipelines|merged results'),
+ mergedResultsBadgeTooltip: s__(
+ 'Pipelines|This pipeline ran on the contents of the merge request combined with the contents of the target branch.',
),
stuckBadgeText: s__('Pipelines|stuck'),
stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
@@ -403,7 +411,7 @@ export default {
{{ commitTitle }}
</h3>
<div>
- <ci-badge-link :status="detailedStatus" class="gl-display-inline-block gl-mb-3" />
+ <ci-icon :status="detailedStatus" show-status-text :show-link="false" class="gl-mb-3" />
<div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
<gl-link
v-if="user"
@@ -458,6 +466,15 @@ export default {
{{ $options.i18n.scheduleBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.trigger"
+ v-gl-tooltip
+ :title="$options.i18n.triggerBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.triggerBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.child"
v-gl-tooltip
:title="$options.i18n.childBadgeTooltip"
@@ -527,6 +544,15 @@ export default {
{{ $options.i18n.detachedBadgeText }}
</gl-badge>
<gl-badge
+ v-if="badges.mergedResultsPipeline"
+ v-gl-tooltip
+ :title="$options.i18n.mergedResultsBadgeTooltip"
+ variant="info"
+ size="sm"
+ >
+ {{ $options.i18n.mergedResultsBadgeText }}
+ </gl-badge>
+ <gl-badge
v-if="badges.stuck"
v-gl-tooltip
:title="$options.i18n.stuckBadgeTooltip"
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index 4752fbb3e96..287f6e045c6 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -5,7 +5,7 @@ import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { TRACKING_CATEGORIES } from '~/ci/constants';
import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
@@ -14,7 +14,7 @@ export default {
fields: DEFAULT_FIELDS,
retry: __('Retry'),
components: {
- CiBadgeLink,
+ CiIcon,
GlButton,
GlLink,
GlTableLite,
@@ -80,7 +80,7 @@ export default {
<div
class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
>
- <ci-badge-link :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <ci-icon :status="item.detailedStatus" class="gl-mr-3" />
<div class="gl-text-truncate">
<gl-link
:href="item.detailedStatus.detailsPath"
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index 067ec3f305e..4966b657887 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -23,9 +23,11 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
failureReason,
triggeredByPath,
schedule,
+ trigger,
child,
latest,
mergeTrainPipeline,
+ mergedResultsPipeline,
invalid,
failed,
autoDevops,
@@ -59,9 +61,11 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
refText,
badges: {
schedule: parseBoolean(schedule),
+ trigger: parseBoolean(trigger),
child: parseBoolean(child),
latest: parseBoolean(latest),
mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
+ mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
invalid: parseBoolean(invalid),
failed: parseBoolean(failed),
autoDevops: parseBoolean(autoDevops),
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
index c3be487caae..63a46d81dd5 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
@@ -2,10 +2,5 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
export const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index 8a7c3367fc1..ea2875713a9 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -42,8 +42,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
projectId,
defaultBranchName,
params,
- iosRunnersAvailable,
- registrationToken,
fullPath,
visibilityPipelineIdType,
} = el.dataset;
@@ -55,7 +53,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
artifactsEndpoint,
artifactsEndpointPlaceholder,
fullPath,
- iosRunnersAvailable: parseBoolean(iosRunnersAvailable),
manualActionsLimit: 50,
pipelineEditorPath,
pipelineSchedulesPath,
@@ -84,7 +81,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
newPipelinePath,
params: JSON.parse(params),
projectId,
- registrationToken,
resetCachePath,
store: this.store,
},
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 8f4d566e7e6..204eaf20664 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -80,7 +80,7 @@ export default {
<template>
<div
- class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-sm-flex-direction-column"
+ class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1 gl-flex-direction-column gl-md-flex-direction-row"
>
<slot></slot>
<gl-button
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 221a45d4d9a..21e21d54758 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -1,13 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { historyPushState } from '~/lib/utils/common_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
@@ -25,17 +17,11 @@ import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/las
export default {
i18n: {
dropdownHeader: __('Switch branch'),
- title: __('Branches'),
fetchError: __('Unable to fetch branch list for this project.'),
},
inputDebounce: BRANCH_SEARCH_DEBOUNCE,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlInfiniteScroll,
- GlLoadingIcon,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -66,6 +52,7 @@ export default {
pageCounter: 0,
searchTerm: '',
lastCommitBranch: '',
+ infiniteScrollLoading: false,
};
},
apollo: {
@@ -112,6 +99,18 @@ export default {
},
},
computed: {
+ infiniteScrollEnabled() {
+ return this.availableBranches.length > 0;
+ },
+ branchesData() {
+ return this.availableBranches.map((branch) => ({
+ text: branch,
+ extraAttrs: {
+ 'data-qa-selector': 'branch_menu_item_button',
+ },
+ value: branch,
+ }));
+ },
availableBranchesVariables() {
if (this.searchTerm.length > 0) {
return {
@@ -128,7 +127,7 @@ export default {
enableBranchSwitcher() {
return this.availableBranches.length > 0 || this.searchTerm.length > 0;
},
- isBranchesLoading() {
+ areBranchesLoading() {
return this.$apollo.queries.availableBranches.loading;
},
},
@@ -143,7 +142,7 @@ export default {
// if there is no searchPattern, paginate by {paginationLimit} branches
fetchNextBranches() {
if (
- this.isBranchesLoading ||
+ this.areBranchesLoading ||
this.searchTerm.length > 0 ||
this.availableBranches.length >= this.totalBranches
) {
@@ -178,16 +177,14 @@ export default {
this.$emit('refetchContent');
},
selectBranch(newBranch) {
- if (newBranch !== this.currentBranch) {
- // If there are unsaved changes, we want to show the user
- // a modal to confirm what to do with these before changing
- // branches.
- if (this.hasUnsavedChanges) {
- this.branchSelected = newBranch;
- this.$emit('select-branch', newBranch);
- } else {
- this.changeBranch(newBranch);
- }
+ // If there are unsaved changes, we want to show the user
+ // a modal to confirm what to do with these before changing
+ // branches.
+ if (this.hasUnsavedChanges) {
+ this.branchSelected = newBranch;
+ this.$emit('select-branch', newBranch);
+ } else {
+ this.changeBranch(newBranch);
}
},
async setSearchTerm(newSearchTerm) {
@@ -211,41 +208,23 @@ export default {
</script>
<template>
- <gl-dropdown
+ <gl-collapsible-listbox
+ v-model="currentBranch"
v-gl-tooltip.hover
+ data-qa-selector="branch_selector_button"
+ searchable
+ :items="branchesData"
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
- :text="currentBranch"
+ :toggle-text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch"
data-testid="branch-selector"
- >
- <gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
- <gl-dropdown-section-header>
- {{ $options.i18n.title }}
- </gl-dropdown-section-header>
-
- <gl-infinite-scroll
- :fetched-items="availableBranches.length"
- :max-list-height="250"
- @bottomReached="fetchNextBranches"
- >
- <template #items>
- <gl-dropdown-item
- v-for="branch in availableBranches"
- :key="branch"
- :is-checked="currentBranch === branch"
- is-check-item
- @click="selectBranch(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- </template>
- <template #default>
- <gl-dropdown-item v-if="isBranchesLoading" key="loading">
- <gl-loading-icon size="lg" />
- </gl-dropdown-item>
- </template>
- </gl-infinite-scroll>
- </gl-dropdown>
+ :no-results-text="$options.i18n.fetchError"
+ :infinite-scroll-loading="areBranchesLoading"
+ :infinite-scroll="infiniteScrollEnabled"
+ @select="selectBranch"
+ @search="setSearchTerm"
+ @bottom-reached="fetchNextBranches"
+ />
</template>
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 44cf11acfe2..7c4a07e3f83 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.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: {
- CiBadgeLink,
+ CiIcon,
GlButton,
GlIcon,
GlLink,
@@ -155,14 +155,7 @@ export default {
</template>
<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-badge-link
- :status="status"
- size="md"
- :show-text="false"
- data-testid="pipeline-status-icon"
- />
- </a>
+ <ci-icon :status="status" data-testid="pipeline-status-icon" />
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
index 922c8eee8fc..340cb6ab979 100644
--- a/app/assets/javascripts/ci/pipeline_editor/options.js
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -55,7 +55,6 @@ export const createAppOptions = (el) => {
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers, {
typeDefs,
- useGet: true,
}),
});
const { cache } = apolloProvider.clients.defaultClient;
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 41e5199e204..09ba6292e13 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
@@ -168,7 +168,7 @@ export default {
@toggle-file-tree="toggleFileTree"
v-on="$listeners"
/>
- <div class="gl-display-flex gl-w-full gl-sm-flex-direction-column">
+ <div class="gl-display-flex gl-w-full gl-flex-direction-column gl-md-flex-direction-row">
<pipeline-editor-file-tree
v-if="showFileTree"
class="gl-flex-shrink-0"
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index d20d4aec59d..4fded3aec60 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -132,7 +132,6 @@ export default {
<template>
<div
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
- data-qa-selector="job_item_container"
>
<gl-link
v-if="hasDetails"
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 34640d49b80..ed78a335453 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.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: {
- CiBadgeLink,
+ CiIcon,
GlLoadingIcon,
GlDropdown,
LegacyJobItem,
@@ -126,14 +126,7 @@ export default {
@show="onShowDropdown"
>
<template #button-content>
- <ci-badge-link
- :status="stage.status"
- size="md"
- :show-text="false"
- :show-tooltip="false"
- :use-link="false"
- class="gl-mb-0!"
- />
+ <ci-icon :status="stage.status" :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">
<gl-loading-icon size="sm" class="gl-mr-3" />
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 cc703d29e23..f6a375ab94c 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.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: {
- CiBadgeLink,
+ CiIcon,
},
inject: {
dataMethod: {
@@ -81,11 +81,6 @@ export default {
// detailedStatus is graphQL, details.status is REST
return pipeline?.detailedStatus || pipeline?.details?.status;
},
- triggerButtonClass(pipeline) {
- const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
-
- return `ci-status-icon-${group}`;
- },
},
};
</script>
@@ -99,15 +94,12 @@ export default {
}"
class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
>
- <ci-badge-link
+ <ci-icon
v-for="pipeline in linkedPipelinesTrimmed"
:key="pipeline.id"
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:status="pipelineStatus(pipeline)"
- size="md"
- :show-text="false"
:show-tooltip="false"
- :class="triggerButtonClass(pipeline)"
class="linked-pipeline-mini-item gl-mb-0!"
data-testid="linked-pipeline-mini-item"
/>
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 2f06b82bac0..722dc29d746 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
@@ -13,9 +13,9 @@ import {
GlSprintf,
GlLoadingIcon,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { fetchPolicies } from '~/lib/graphql';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
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 cd1d9a97ef3..5444e66cbdf 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
@@ -371,11 +371,7 @@ export default {
</gl-form-group>
<!--Variable List-->
<gl-form-group class="gl-mb-0" :label="$options.i18n.variables">
- <div
- v-for="(variable, index) in variables"
- :key="`var-${index}`"
- data-qa-selector="ci_variable_row_container"
- >
+ <div v-for="(variable, index) in variables" :key="`var-${index}`">
<div
v-if="!variable.destroy"
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
index ed7c2bbeb73..78df7298f4f 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -61,6 +61,7 @@ export default {
v-if="canPlay"
v-gl-tooltip
:title="$options.i18n.playTooltip"
+ :aria-label="$options.i18n.playTooltip"
icon="play"
data-testid="play-pipeline-schedule-btn"
@click="$emit('playPipelineSchedule', schedule.id)"
@@ -78,6 +79,7 @@ export default {
v-gl-tooltip
:href="editPathWithIdParam"
:title="$options.i18n.editTooltip"
+ :aria-label="$options.i18n.editTooltip"
icon="pencil"
data-testid="edit-pipeline-schedule-btn"
/>
@@ -85,6 +87,7 @@ export default {
v-if="canRemove"
v-gl-tooltip
:title="$options.i18n.deleteTooltip"
+ :aria-label="$options.i18n.deleteTooltip"
icon="remove"
variant="danger"
data-testid="delete-pipeline-schedule-btn"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 92f461c72d7..d979c0efaf2 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,9 +1,9 @@
<script>
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
},
props: {
schedule: {
@@ -24,9 +24,10 @@ export default {
<template>
<div data-testid="last-pipeline-status">
- <ci-badge-link
+ <ci-icon
v-if="hasPipeline"
:status="lastPipelineStatus"
+ show-status-text
class="gl-vertical-align-middle"
/>
<span v-else data-testid="pipeline-schedule-status-text">
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
deleted file mode 100644
index 1a2021df9c8..00000000000
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
+++ /dev/null
@@ -1,220 +0,0 @@
-<script>
-import { GlButton, GlCard, GlSprintf, GlLink, GlPopover, GlModalDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { mergeUrlParams, DOCS_URL } from '~/lib/utils/url_utility';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import apolloProvider from '~/ci/pipeline_details/graphql/provider';
-import CiTemplates from './ci_templates.vue';
-
-export default {
- components: {
- GlButton,
- GlCard,
- GlSprintf,
- GlLink,
- GlPopover,
- RunnerInstructionsModal,
- CiTemplates,
- },
- directives: {
- GlModalDirective,
- },
- inject: ['pipelineEditorPath', 'iosRunnersAvailable'],
- props: {
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
- },
- apolloProvider,
- iOSTemplateName: 'iOS-Fastlane',
- modalId: 'runner-instructions-modal',
- runnerDocsLink: `${DOCS_URL}/runner/install/osx`,
- whatElseLink: helpPagePath('ci/index.md'),
- i18n: {
- title: s__('Pipelines|Get started with GitLab CI/CD'),
- subtitle: s__('Pipelines|Building for iOS?'),
- explanation: s__("Pipelines|We'll walk you through how to deploy to iOS in two easy steps."),
- runnerSetupTitle: s__('Pipelines|1. Set up a runner'),
- runnerSetupButton: s__('Pipelines|Set up a runner'),
- runnerSetupBodyUnfinished: s__(
- 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline.',
- ),
- runnerSetupBodyFinished: s__(
- 'Pipelines|You have runners available to run your job now. No need to do anything else.',
- ),
- runnerSetupPopoverTitle: s__(
- "Pipelines|Let's get that runner set up! %{emojiStart}tada%{emojiEnd}",
- ),
- runnerSetupPopoverBodyLine1: s__(
- 'Pipelines|Follow these instructions to install GitLab Runner on macOS.',
- ),
- runnerSetupPopoverBodyLine2: s__(
- 'Pipelines|Need more information to set up your runner? %{linkStart}Check out our documentation%{linkEnd}.',
- ),
- configurePipelineTitle: s__('Pipelines|2. Configure deployment pipeline'),
- configurePipelineBody: s__("Pipelines|We'll guide you through a simple pipeline set-up."),
- configurePipelineButton: s__('Pipelines|Configure pipeline'),
- noWalkthroughTitle: s__("Pipelines|Don't need a guide? Jump in right away with a template."),
- noWalkthroughExplanation: s__('Pipelines|Based on your project, we recommend this template:'),
- notBuildingForIos: s__(
- "Pipelines|Not building for iOS or not what you're looking for? %{linkStart}See what else%{linkEnd} GitLab CI/CD has to offer.",
- ),
- },
- data() {
- return {
- isModalShown: false,
- isPopoverShown: false,
- isRunnerSetupFinished: this.iosRunnersAvailable,
- popoverTarget: `${this.$options.modalId}___BV_modal_content_`,
- configurePipelineLink: mergeUrlParams(
- { template: this.$options.iOSTemplateName },
- this.pipelineEditorPath,
- ),
- };
- },
- computed: {
- runnerSetupBodyText() {
- return this.iosRunnersAvailable
- ? this.$options.i18n.runnerSetupBodyFinished
- : this.$options.i18n.runnerSetupBodyUnfinished;
- },
- },
- methods: {
- showModal() {
- this.isModalShown = true;
- },
- hideModal() {
- this.togglePopover();
- this.isRunnerSetupFinished = true;
- },
- togglePopover() {
- this.isPopoverShown = !this.isPopoverShown;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.title }}</h2>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-1">{{ $options.i18n.subtitle }}</h3>
- <p>{{ $options.i18n.explanation }}</p>
-
- <div class="gl-lg-display-flex">
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5">
- <gl-emoji
- v-show="isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="white_check_mark"
- data-testid="runner-setup-marked-completed"
- />
- <gl-emoji
- v-show="!isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="tools"
- data-testid="runner-setup-marked-todo"
- />
- </div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.runnerSetupTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ runnerSetupBodyText }}</p>
- </div>
-
- <gl-button
- v-if="!iosRunnersAvailable"
- v-gl-modal-directive="$options.modalId"
- category="primary"
- variant="confirm"
- @click="showModal"
- >
- {{ $options.i18n.runnerSetupButton }}
- </gl-button>
- <runner-instructions-modal
- v-if="isModalShown"
- :modal-id="$options.modalId"
- :registration-token="registrationToken"
- default-platform-name="osx"
- @shown="togglePopover"
- @hide="hideModal"
- />
- <gl-popover
- v-if="isPopoverShown"
- :show="true"
- :show-close-button="true"
- :target="popoverTarget"
- triggers="manual"
- placement="left"
- fallback-placement="clockwise"
- >
- <template #title>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverTitle">
- <template #emoji="{ content }">
- <gl-emoji class="gl-ml-2" :data-name="content" />
- </template>
- </gl-sprintf>
- </template>
- <div class="gl-mb-5">
- {{ $options.i18n.runnerSetupPopoverBodyLine1 }}
- </div>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverBodyLine2">
- <template #link="{ content }">
- <gl-link :href="$options.runnerDocsLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-popover>
- </div>
- </gl-card>
- </div>
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="tools" /></div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.configurePipelineTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ $options.i18n.configurePipelineBody }}</p>
- </div>
-
- <gl-button
- :disabled="!isRunnerSetupFinished"
- category="primary"
- variant="confirm"
- data-testid="configure-pipeline-link"
- :href="configurePipelineLink"
- >
- {{ $options.i18n.configurePipelineButton }}
- </gl-button>
- </div>
- </gl-card>
- </div>
- </div>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-5">{{ $options.i18n.noWalkthroughTitle }}</h3>
- <p>{{ $options.i18n.noWalkthroughExplanation }}</p>
- <ci-templates
- :filter-templates="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- $options.iOSTemplateName,
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- :disabled="!isRunnerSetupFinished"
- />
- <p>
- <gl-sprintf :message="$options.i18n.notBuildingForIos">
- <template #link="{ content }">
- <gl-link :href="$options.whatElseLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-</template>
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 728e8541ae3..aed5f1d235d 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
@@ -1,9 +1,7 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
-import IosTemplates from './ios_templates.vue';
export default {
i18n: {
@@ -12,9 +10,7 @@ export default {
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
- GitlabExperiment,
PipelinesCiTemplates,
- IosTemplates,
},
props: {
emptyStateSvgPath: {
@@ -25,30 +21,15 @@ export default {
type: Boolean,
required: true,
},
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
},
};
</script>
<template>
- <div>
- <gitlab-experiment v-if="canSetCi" name="ios_specific_templates">
- <template #control>
- <pipelines-ci-templates />
- </template>
- <template #candidate>
- <ios-templates :registration-token="registrationToken" />
- </template>
- </gitlab-experiment>
- <gl-empty-state
- v-else
- title=""
- :svg-path="emptyStateSvgPath"
- :svg-height="null"
- :description="$options.i18n.noCiDescription"
- />
- </div>
+ <pipelines-ci-templates v-if="canSetCi" />
+ <gl-empty-state
+ v-else
+ :svg-path="emptyStateSvgPath"
+ :svg-height="null"
+ :description="$options.i18n.noCiDescription"
+ />
</template>
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 8f45094eb74..31d8f207a63 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { SCHEDULE_ORIGIN } from '~/ci/pipeline_details/constants';
+import { SCHEDULE_ORIGIN, API_ORIGIN, TRIGGER_ORIGIN } from '../constants';
export default {
components: {
@@ -31,6 +31,9 @@ export default {
isScheduled() {
return this.pipeline.source === SCHEDULE_ORIGIN;
},
+ isTriggered() {
+ return this.pipeline.source === TRIGGER_ORIGIN;
+ },
isInFork() {
return Boolean(
this.targetProjectFullPath &&
@@ -50,6 +53,9 @@ export default {
autoDevopsHelpPath() {
return helpPagePath('topics/autodevops/index.md');
},
+ isApi() {
+ return this.pipeline.source === API_ORIGIN;
+ },
},
};
</script>
@@ -64,7 +70,16 @@ export default {
variant="info"
size="sm"
data-testid="pipeline-url-scheduled"
- >{{ __('Scheduled') }}</gl-badge
+ >{{ __('scheduled') }}</gl-badge
+ >
+ <gl-badge
+ v-if="isTriggered"
+ v-gl-tooltip
+ :title="__('This pipeline was created by an API call authenticated with a trigger token')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-triggered"
+ >{{ __('trigger token') }}</gl-badge
>
<gl-badge
v-if="pipeline.flags.latest"
@@ -185,5 +200,14 @@ export default {
data-testid="pipeline-url-fork"
>{{ __('fork') }}</gl-badge
>
+ <gl-badge
+ v-if="isApi"
+ v-gl-tooltip
+ :title="__('This pipeline was triggered using the api')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-api-badge"
+ >{{ s__('Pipeline|api') }}</gl-badge
+ >
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 20e2c7e9dce..380f8ce172f 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,12 +1,12 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
PipelinesTimeago,
},
mixins: [Tracking.mixin()],
@@ -31,7 +31,12 @@ export default {
<template>
<div>
- <ci-badge-link class="gl-mb-3" :status="pipelineStatus" @ciStatusBadgeClick="trackClick" />
+ <ci-icon
+ class="gl-mb-2"
+ :status="pipelineStatus"
+ show-status-text
+ @ciStatusBadgeClick="trackClick"
+ />
<pipelines-timeago :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
index 2a73795db0a..a53c7cacae2 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
@@ -29,9 +29,5 @@ export default {
<gl-avatar-link v-if="user" v-gl-tooltip :href="user.path" :title="user.name" class="gl-ml-3">
<gl-avatar :size="32" :src="user.avatar_url" />
</gl-avatar-link>
-
- <span v-else class="gl-ml-3">
- {{ s__('Pipelines|API') }}
- </span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipelines_page/constants.js b/app/assets/javascripts/ci/pipelines_page/constants.js
index aa6ef8a25ee..438eda44afe 100644
--- a/app/assets/javascripts/ci/pipelines_page/constants.js
+++ b/app/assets/javascripts/ci/pipelines_page/constants.js
@@ -1,2 +1,5 @@
export const ANY_TRIGGER_AUTHOR = 'Any';
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
+export const SCHEDULE_ORIGIN = 'schedule';
+export const API_ORIGIN = 'api';
+export const TRIGGER_ORIGIN = 'trigger';
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index faa013079be..98e005a162f 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -4,7 +4,7 @@ import NO_PIPELINES_SVG from '@gitlab/svgs/dist/illustrations/empty-state/empty-
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';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/alert';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -88,11 +88,6 @@ export default {
type: Object,
required: true,
},
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
defaultVisibilityPipelineIdType: {
type: String,
required: false,
@@ -311,6 +306,12 @@ export default {
},
changeVisibilityPipelineIDType(idType) {
this.visibilityPipelineIdType = idType;
+ if (idType === PIPELINE_IID_KEY) {
+ this.track('pipelines_display_options', {
+ label: TRACKING_CATEGORIES.listbox,
+ property: idType,
+ });
+ }
if (isLoggedIn()) {
this.saveVisibilityPipelineIDType(idType);
@@ -404,7 +405,6 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="$options.noPipelinesSvgPath"
:can-set-ci="canCreatePipeline"
- :registration-token="registrationToken"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
index f0a41a5949e..97163c1f55c 100644
--- a/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_new_runner/admin_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, INSTANCE_TYPE } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'AdminNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -44,8 +42,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New instance runner') }}</h1>
<registration-compatibility-alert :alert-key="$options.INSTANCE_TYPE" />
diff --git a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index 0ec94dc865f..1431f156c0e 100644
--- a/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -14,6 +14,7 @@ import {
import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import RunnerListHeader from '../components/runner_list_header.vue';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@@ -28,6 +29,7 @@ import RunnerJobStatusBadge from '../components/runner_job_status_badge.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
+import { versionTokenConfig } from '../components/search_tokens/version_token_config';
import {
ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
@@ -42,6 +44,7 @@ export default {
components: {
GlButton,
GlLink,
+ RunnerListHeader,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
@@ -78,9 +81,6 @@ export default {
apollo: {
runners: {
query: allRunnersQuery,
- context: {
- isSingleRequest: true,
- },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -118,6 +118,7 @@ export default {
return [
pausedTokenConfig,
statusTokenConfig,
+ versionTokenConfig,
{
...tagTokenConfig,
recentSuggestionsStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
@@ -178,11 +179,9 @@ export default {
</script>
<template>
<div>
- <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
- <h2 class="gl-my-0 header-title">
- {{ s__('Runners|Runners') }}
- </h2>
- <div class="gl-display-flex gl-gap-3">
+ <runner-list-header>
+ <template #title>{{ s__('Runners|Runners') }}</template>
+ <template #actions>
<runner-dashboard-link />
<gl-button :href="newRunnerPath" variant="confirm">
{{ s__('Runners|New instance runner') }}
@@ -192,8 +191,9 @@ export default {
:type="$options.INSTANCE_TYPE"
placement="right"
/>
- </div>
- </header>
+ </template>
+ </runner-list-header>
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index a80d6207be8..8a920c85e06 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -2,9 +2,9 @@
import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, __, formatNumber } from '~/locale';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import RunnerCreatedAt from '../runner_created_at.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
@@ -15,8 +15,6 @@ import {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
} from '../../constants';
import RunnerSummaryField from './runner_summary_field.vue';
@@ -26,13 +24,13 @@ export default {
GlSprintf,
TimeAgo,
RunnerSummaryField,
+ RunnerCreatedAt,
RunnerName,
RunnerTags,
RunnerTypeBadge,
RunnerManagersBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
- UserAvatarLink,
TooltipOnTruncate,
},
directives: {
@@ -75,8 +73,6 @@ export default {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
- I18N_CREATED_AT_LABEL,
- I18N_CREATED_AT_BY_LABEL,
},
};
</script>
@@ -143,30 +139,7 @@ export default {
</runner-summary-field>
<runner-summary-field icon="calendar">
- <template v-if="createdBy">
- <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_BY_LABEL">
- <template #timeAgo>
- <time-ago v-if="runner.createdAt" :time="runner.createdAt" />
- </template>
- <template #avatar>
- <user-avatar-link
- :link-href="createdBy.webUrl"
- :img-src="createdBy.avatarUrl"
- img-css-classes="gl-vertical-align-top"
- :img-size="16"
- :img-alt="createdByImgAlt"
- :tooltip-text="createdBy.username"
- />
- </template>
- </gl-sprintf>
- </template>
- <template v-else>
- <gl-sprintf :message="$options.i18n.I18N_CREATED_AT_LABEL">
- <template #timeAgo>
- <time-ago v-if="runner.createdAt" :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </template>
+ <runner-created-at :runner="runner" />
</runner-summary-field>
</div>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue b/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue
deleted file mode 100644
index 6fd4edf5847..00000000000
--- a/app/assets/javascripts/ci/runner/components/registration/registration_feedback_banner.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import ILLUSTRATION_URL from '@gitlab/svgs/dist/illustrations/rocket-launch-md.svg?url';
-import { GlBanner } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-
-const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/387993';
-
-export default {
- components: {
- GlBanner,
- UserCalloutDismisser,
- },
- i18n: {
- title: s__("Runners|We've made some changes and want your feedback"),
- body: s__(
- "Runners|We've been making improvements to how you register runners so that it's more secure and efficient. Tell us how we're doing.",
- ),
- button: s__('Runners|Add your feedback to this issue'),
- },
- ILLUSTRATION_URL,
- FEEDBACK_ISSUE_URL,
-};
-</script>
-<template>
- <user-callout-dismisser feature-name="create_runner_workflow_banner">
- <template #default="{ dismiss, shouldShowCallout }">
- <gl-banner
- v-if="shouldShowCallout"
- class="gl-my-6"
- :title="$options.i18n.title"
- :svg-path="$options.ILLUSTRATION_URL"
- :button-text="$options.i18n.button"
- :button-link="$options.FEEDBACK_ISSUE_URL"
- @close="dismiss"
- >
- <p>{{ $options.i18n.body }}</p>
- </gl-banner>
- </template>
- </user-callout-dismisser>
-</template>
diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
index 771ecb1a0d4..a4dec8199a3 100644
--- a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue
@@ -100,11 +100,11 @@ export default {
tokenMessage() {
if (this.token) {
return s__(
- 'Runners|The %{boldStart}runner token%{boldEnd} %{token} displays %{boldStart}only for a short time%{boldEnd}, and is stored in the %{codeStart}config.toml%{codeEnd} after you register the runner. It will not be visible once the runner is registered.',
+ 'Runners|The %{boldStart}runner authentication token%{boldEnd} %{token} displays here %{boldStart}for a short time only%{boldEnd}. After you register the runner, this token is stored in the %{codeStart}config.toml%{codeEnd} and cannot be accessed again from the UI.',
);
}
return s__(
- 'Runners|The %{boldStart}runner token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.',
+ 'Runners|The %{boldStart}runner authentication token%{boldEnd} is no longer visible, it is stored in the %{codeStart}config.toml%{codeEnd} if you have registered the runner.',
);
},
commandPrompt() {
diff --git a/app/assets/javascripts/ci/runner/components/runner_created_at.vue b/app/assets/javascripts/ci/runner/components/runner_created_at.vue
new file mode 100644
index 00000000000..410142a0eb5
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_created_at.vue
@@ -0,0 +1,72 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import {
+ I18N_CREATED_AT_LABEL,
+ I18N_CREATED_BY_LABEL,
+ I18N_CREATED_BY_AT_LABEL,
+} from '../constants';
+
+export default {
+ components: {
+ GlSprintf,
+ GlLink,
+ TimeAgo,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ createdAt() {
+ return this.runner?.createdAt;
+ },
+ createdBy() {
+ return this.runner?.createdBy;
+ },
+ createdById() {
+ if (this.createdBy?.id) {
+ return getIdFromGraphQLId(this.createdBy.id);
+ }
+ return null;
+ },
+ message() {
+ if (this.createdBy && this.createdAt) {
+ return I18N_CREATED_BY_AT_LABEL;
+ }
+ if (this.createdBy) {
+ return I18N_CREATED_BY_LABEL;
+ }
+ if (this.createdAt) {
+ return I18N_CREATED_AT_LABEL;
+ }
+
+ return null;
+ },
+ },
+};
+</script>
+<template>
+ <span v-if="message">
+ <gl-sprintf :message="message">
+ <template #timeAgo>
+ <time-ago v-if="createdAt" :time="createdAt" />
+ </template>
+ <template #user>
+ <gl-link
+ class="js-user-link gl-reset-color gl-font-weight-bold"
+ :href="createdBy.webUrl"
+ :data-user-id="createdById"
+ :data-username="createdBy.username"
+ :data-name="createdBy.name"
+ :data-avatar-url="createdBy.avatarUrl"
+ >{{ createdBy.name }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index 0ec2ef30c20..477d28c6c28 100644
--- a/app/assets/javascripts/ci/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -120,12 +120,9 @@ export default {
}}
</p>
<p class="gl-mb-0">
- <gl-link
- :href="tokenExpirationHelpUrl"
- target="_blank"
- class="gl-reset-font-size"
- >{{ __('Learn more') }}</gl-link
- >
+ <gl-link :href="tokenExpirationHelpUrl" target="_blank">{{
+ __('Learn more')
+ }}</gl-link>
</p>
</help-popover>
</template>
@@ -156,12 +153,9 @@ export default {
"
>
<template #link="{ content }"
- ><gl-link
- :href="$options.RUNNER_MANAGERS_HELP_URL"
- target="_blank"
- class="gl-reset-font-size"
- >{{ content }}</gl-link
- ></template
+ ><gl-link :href="$options.RUNNER_MANAGERS_HELP_URL" target="_blank">{{
+ content
+ }}</gl-link></template
>
</gl-sprintf>
</help-popover>
diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 0fa06537ed6..f8d0352e532 100644
--- a/app/assets/javascripts/ci/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
@@ -1,16 +1,15 @@
<script>
-import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../constants';
import { formatRunnerName } from '../utils';
+import RunnerCreatedAt from './runner_created_at.vue';
import RunnerTypeBadge from './runner_type_badge.vue';
import RunnerStatusBadge from './runner_status_badge.vue';
export default {
components: {
GlIcon,
- GlSprintf,
- TimeAgo,
+ RunnerCreatedAt,
RunnerTypeBadge,
RunnerStatusBadge,
RunnerUpgradeStatusBadge: () =>
@@ -43,21 +42,13 @@ export default {
<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>
- <gl-icon
- v-if="runner.locked"
- v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- name="lock"
- :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
- />
- </template>
- <template #timeago>
- <time-ago :time="runner.createdAt" />
- </template>
- </gl-sprintf>
- </span>
+ <gl-icon
+ v-if="runner.locked"
+ v-gl-tooltip="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ name="lock"
+ :aria-label="$options.I18N_LOCKED_RUNNER_DESCRIPTION"
+ />
+ <runner-created-at :runner="runner" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index 5d8e9dcdee2..653d9b05330 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
@@ -11,7 +11,7 @@ import LinkCell from './cells/link_cell.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
GlTableLite,
LinkCell,
RunnerTags,
@@ -80,7 +80,7 @@ export default {
fixed
>
<template #cell(status)="{ item = {} }">
- <ci-badge-link v-if="item.detailedStatus" :status="item.detailedStatus" />
+ <ci-icon v-if="item.detailedStatus" :status="item.detailedStatus" show-status-text />
</template>
<template #cell(job)="{ item = {} }">
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_header.vue b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
new file mode 100644
index 00000000000..e4367db035e
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
@@ -0,0 +1,17 @@
+<script>
+export default {
+ name: 'RunnerListHeader',
+};
+</script>
+<template>
+ <header
+ class="gl-my-5 gl-display-flex gl-align-items-flex-start gl-flex-wrap gl-justify-content-space-between"
+ >
+ <h1 v-if="$scopedSlots.title" class="gl-my-0 gl-font-size-h1 header-title">
+ <slot name="title"></slot>
+ </h1>
+ <div v-if="$scopedSlots.actions" class="gl-display-flex gl-gap-3">
+ <slot name="actions"></slot>
+ </div>
+ </header>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
index dd1cca0a05c..1f61e878eb0 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
@@ -70,7 +70,7 @@ export default {
@fetch-suggestions="fetchTags"
v-on="$listeners"
>
- <template #view-token="{ viewTokenProps: { listeners, inputValue, activeTokenValue } }">
+ <template #view-token="{ viewTokenProps: { listeners = {}, inputValue, activeTokenValue } }">
<gl-token variant="search-value" :class="$options.RUNNER_TAG_BG_CLASS" v-on="listeners">
{{ activeTokenValue ? activeTokenValue.text : inputValue }}
</gl-token>
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js
new file mode 100644
index 00000000000..23f82d06f6d
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/version_token_config.js
@@ -0,0 +1,12 @@
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { PARAM_KEY_VERSION, I18N_VERSION } from '../../constants';
+
+export const versionTokenConfig = {
+ icon: 'doc-versions',
+ title: I18N_VERSION,
+ type: PARAM_KEY_VERSION,
+ token: BaseToken,
+ operators: OPERATORS_IS,
+ suggestionsDisabled: true,
+};
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index b3cc295f8e4..d04d75b6e75 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -99,10 +99,14 @@ export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
'Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.',
);
+export const I18N_VERSION = s__('Runners|Version starts with');
export const I18N_VERSION_LABEL = s__('Runners|Version %{version}');
export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}');
+
export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}');
-export const I18N_CREATED_AT_BY_LABEL = s__('Runners|Created %{timeAgo} by %{avatar}');
+export const I18N_CREATED_BY_LABEL = s__('Runners|Created by %{user}');
+export const I18N_CREATED_BY_AT_LABEL = s__('Runners|Created by %{user} %{timeAgo}');
+
export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited');
export const I18N_ADMIN = s__('Runners|Administrator');
@@ -154,6 +158,7 @@ export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_PAUSED = 'paused';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
+export const PARAM_KEY_VERSION = 'version_prefix';
export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_MEMBERSHIP = 'membership';
diff --git a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
index 41ec9967d90..5aa96f42b04 100644
--- a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment RunnerFieldsShared on CiRunner {
id
shortSha
@@ -10,5 +12,8 @@ fragment RunnerFieldsShared on CiRunner {
maximumTimeout
tagList
createdAt
+ createdBy {
+ ...User
+ }
status
}
diff --git a/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
index 15401c25c64..628ebfd2029 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
@@ -10,6 +10,7 @@ query getAllRunners(
$type: CiRunnerType
$tagList: [String!]
$search: String
+ $versionPrefix: String
$sort: CiRunnerSort
) {
runners(
@@ -22,6 +23,7 @@ query getAllRunners(
type: $type
tagList: $tagList
search: $search
+ versionPrefix: $versionPrefix
sort: $sort
) {
...AllRunnersConnection
diff --git a/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
index 82591b88d3e..18f587495b0 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
@@ -4,8 +4,16 @@ query getAllRunnersCount(
$type: CiRunnerType
$tagList: [String!]
$search: String
+ $versionPrefix: String
) {
- runners(paused: $paused, status: $status, type: $type, tagList: $tagList, search: $search) {
+ runners(
+ paused: $paused
+ status: $status
+ type: $type
+ tagList: $tagList
+ search: $search
+ versionPrefix: $versionPrefix
+ ) {
count
}
}
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 e2c890b3834..8f998ab42fa 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
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment RunnerDetailsShared on CiRunner {
id
shortSha
@@ -11,6 +13,9 @@ fragment RunnerDetailsShared on CiRunner {
jobCount
tagList
createdAt
+ createdBy {
+ ...User
+ }
status
contactedAt
tokenExpiresAt
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
index b6d6996a857..611de43b995 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
@@ -8,7 +8,7 @@ query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String,
nodes {
id
detailedStatus {
- # fields for `<ci-badge-link>`
+ # fields for `<ci-icon>`
id
detailsPath
group
diff --git a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
index 2e1706ddae9..c907f9c8982 100644
--- a/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/group_new_runner/group_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, GROUP_TYPE, PARAM_KEY_PLATFORM } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'GroupNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -50,8 +48,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New group runner') }}</h1>
<registration-compatibility-alert :alert-key="groupId" />
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index dcaf8635f5c..b5042936b1e 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -14,6 +14,7 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
+import RunnerListHeader from '../components/runner_list_header.vue';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
@@ -44,6 +45,7 @@ export default {
components: {
GlButton,
GlLink,
+ RunnerListHeader,
RegistrationDropdown,
RunnerFilteredSearchBar,
RunnerList,
@@ -86,9 +88,6 @@ export default {
apollo: {
runners: {
query: groupRunnersQuery,
- context: {
- isSingleRequest: true,
- },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -212,11 +211,9 @@ export default {
<template>
<div>
- <header class="gl-my-5 gl-display-flex gl-justify-content-space-between">
- <h2 class="gl-my-0 header-title">
- {{ s__('Runners|Runners') }}
- </h2>
- <div class="gl-display-flex gl-gap-3">
+ <runner-list-header>
+ <template #title>{{ s__('Runners|Runners') }}</template>
+ <template #actions>
<gl-button
v-if="newRunnerPath"
:href="newRunnerPath"
@@ -231,8 +228,9 @@ export default {
:type="$options.GROUP_TYPE"
placement="right"
/>
- </div>
- </header>
+ </template>
+ </runner-list-header>
+
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
index 51f5a9ce8d9..241479a8c98 100644
--- a/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
+++ b/app/assets/javascripts/ci/runner/project_new_runner/project_new_runner_app.vue
@@ -4,7 +4,6 @@ import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import RegistrationCompatibilityAlert from '~/ci/runner/components/registration/registration_compatibility_alert.vue';
-import RegistrationFeedbackBanner from '~/ci/runner/components/registration/registration_feedback_banner.vue';
import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { DEFAULT_PLATFORM, PARAM_KEY_PLATFORM, PROJECT_TYPE } from '../constants';
@@ -14,7 +13,6 @@ export default {
name: 'ProjectNewRunnerApp',
components: {
RegistrationCompatibilityAlert,
- RegistrationFeedbackBanner,
RunnerPlatformsRadioGroup,
RunnerCreateForm,
},
@@ -50,8 +48,6 @@ export default {
<template>
<div>
- <registration-feedback-banner />
-
<h1 class="gl-font-size-h2">{{ s__('Runners|New project runner') }}</h1>
<registration-compatibility-alert :alert-key="projectId" />
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index 8915198350f..e3aee15f42c 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -12,6 +12,7 @@ import {
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
PARAM_KEY_SEARCH,
PARAM_KEY_MEMBERSHIP,
PARAM_KEY_SORT,
@@ -151,7 +152,12 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
membership: membership || DEFAULT_MEMBERSHIP,
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG],
+ filterNamesAllowList: [
+ PARAM_KEY_PAUSED,
+ PARAM_KEY_STATUS,
+ PARAM_KEY_TAG,
+ PARAM_KEY_VERSION,
+ ],
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
),
@@ -178,6 +184,7 @@ export const fromSearchToUrl = (
[PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
[PARAM_KEY_PAUSED]: [],
+ [PARAM_KEY_VERSION]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
@@ -229,6 +236,7 @@ export const fromSearchToVariables = ({
[filterVariables.status] = queryObj[PARAM_KEY_STATUS] || [];
filterVariables.search = queryObj[PARAM_KEY_SEARCH];
filterVariables.tagList = queryObj[PARAM_KEY_TAG];
+ [filterVariables.versionPrefix] = queryObj[PARAM_KEY_VERSION] || [];
if (queryObj[PARAM_KEY_PAUSED]) {
filterVariables.paused = parseBoolean(queryObj[PARAM_KEY_PAUSED]);
diff --git a/app/assets/javascripts/ci/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 25fecdcfa7d..01a20880e0a 100644
--- a/app/assets/javascripts/ci/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
const COMPONENT_TAG = 'vue_component';
diff --git a/app/assets/javascripts/ci/utils.js b/app/assets/javascripts/ci/utils.js
index 8a4f28404c6..21361aedb9d 100644
--- a/app/assets/javascripts/ci/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
export const reportToSentry = (component, failureType) => {
Sentry.captureException(failureType, {
diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
index 509bdabdd9e..fb2e24e15f6 100644
--- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
+++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
@@ -12,7 +12,7 @@ import {
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index 0871d543d46..e1f6006fedf 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -180,17 +180,11 @@ export default {
data-confirm-btn-variant="danger"
rel="nofollow"
data-testid="trigger_revoke_button"
- data-qa-selector="trigger_revoke_button"
:href="item.projectTriggerPath"
/>
</template>
</gl-table>
- <div
- v-else
- class="gl-new-card-empty gl-px-5 gl-py-4"
- data-testid="no_triggers_content"
- data-qa-selector="no_triggers_content"
- >
+ <div v-else class="gl-new-card-empty gl-px-5 gl-py-4" data-testid="no_triggers_content">
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
</div>
</div>
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 4537fd51fcf..f474e51622a 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
index 4a9f79460da..fe6142ae145 100644
--- a/app/assets/javascripts/commons/gitlab_ui.js
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -5,6 +5,8 @@ applyGitLabUIConfig({
translations: {
'GlSearchBoxByType.input.placeholder': __('Search'),
'GlSearchBoxByType.clearButtonTitle': __('Clear'),
+ 'GlSorting.sortAscending': __('Sort direction: Ascending'),
+ 'GlSorting.sortDescending': __('Sort direction: Descending'),
'ClearIconButton.title': __('Clear'),
},
});
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index 6535d9eaa5d..b34ebe85eb4 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -161,6 +161,8 @@ export default {
},
onKeyDown({ event }) {
+ if (!this.items.length) return false;
+
if (event.key === 'ArrowUp') {
this.upHandler();
return true;
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
index 1aa6568848f..6f7e9653e6e 100644
--- a/app/assets/javascripts/content_editor/content_editor.stories.js
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -30,4 +30,5 @@ Default.args = {
serializerConfig: {},
extensions: [],
enableAutocomplete: false,
+ markdownDocsPath: 'fake/path',
};
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 8917417e55e..da5ac7eb158 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -81,4 +81,13 @@ export default CodeBlockLowlight.extend({
addNodeView() {
return new VueNodeViewRenderer(CodeBlockWrapper);
},
+
+ addProseMirrorPlugins() {
+ const parentPlugins = this.parent?.() ?? [];
+ // We don't want TipTap's VSCode paste plugin to be loaded since
+ // it conflicts with our CopyPaste plugin.
+ const i = parentPlugins.findIndex((plugin) => plugin.key.includes('VSCode'));
+ if (i >= 0) parentPlugins.splice(i, 1);
+ return parentPlugins;
+ },
}).configure({ lowlight });
diff --git a/app/assets/javascripts/content_editor/extensions/copy_paste.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js
index ab9e5619600..d29a407c5ca 100644
--- a/app/assets/javascripts/content_editor/extensions/copy_paste.js
+++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js
@@ -11,6 +11,7 @@ import CodeBlockHighlight from './code_block_highlight';
import CodeSuggestion from './code_suggestion';
import Diagram from './diagram';
import Frontmatter from './frontmatter';
+import { loadingPlugin, findLoader } from './loading';
const TEXT_FORMAT = 'text/plain';
const GFM_FORMAT = 'text/x-gfm';
@@ -31,21 +32,6 @@ function parseHTML(schema, html) {
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
}
-const findLoader = (editor, loaderId) => {
- let position;
-
- editor.view.state.doc.descendants((descendant, pos) => {
- if (descendant.type.name === 'loading' && descendant.attrs.id === loaderId) {
- position = pos;
- return false;
- }
-
- return true;
- });
-
- return position;
-};
-
export default Extension.create({
name: 'copyPaste',
priority: EXTENSION_PRIORITY_HIGHEST,
@@ -74,13 +60,20 @@ export default Extension.create({
Promise.resolve()
.then(() => {
- editor.commands.insertContent({ type: 'loading', attrs: { id: loaderId } });
+ editor
+ .chain()
+ .deleteSelection()
+ .setMeta(loadingPlugin, {
+ add: { loaderId, pos: editor.state.selection.from },
+ })
+ .run();
+
return promise;
})
.then(async ({ document }) => {
if (!document) return;
- const pos = findLoader(editor, loaderId);
+ const pos = findLoader(editor.state, loaderId);
if (!pos) return;
const { firstChild, childCount } = document.content;
@@ -91,7 +84,7 @@ export default Extension.create({
editor
.chain()
- .deleteRange({ from: pos, to: pos + 1 })
+ .setMeta(loadingPlugin, { remove: { loaderId } })
.insertContentAt(pos, toPaste.toJSON(), {
updateSelection: false,
})
@@ -113,7 +106,16 @@ export default Extension.create({
const handleCutAndCopy = (view, event) => {
const slice = view.state.selection.content();
- const gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ let gfmContent = this.options.serializer.serialize({ doc: slice.content });
+ const gfmContentWithoutSingleTableCell = gfmContent.replace(
+ /^<table>[\s\n]*<tr>[\s\n]*<t[hd]>|<\/t[hd]>[\s\n]*<\/tr>[\s\n]*<\/table>[\s\n]*$/gim,
+ '',
+ );
+ const containsSingleTableCell = !/<t[hd]>/.test(gfmContentWithoutSingleTableCell);
+
+ if (containsSingleTableCell) {
+ gfmContent = gfmContentWithoutSingleTableCell;
+ }
const documentFragment = DOMSerializer.fromSchema(view.state.schema).serializeFragment(
slice.content,
);
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index 7f8b5da5f46..be6ecb6cafd 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -1,5 +1,5 @@
import { Node, InputRule } from '@tiptap/core';
-import { initEmojiMap, getAllEmoji } from '~/emoji';
+import { initEmojiMap, getEmojiMap } from '~/emoji';
export default Node.create({
name: 'emoji',
@@ -58,7 +58,7 @@ export default Node.create({
find: emojiInputRegex,
handler: ({ state, range: { from, to }, match }) => {
const [, , name] = match;
- const emojis = getAllEmoji();
+ const emojis = getEmojiMap();
const emoji = emojis[name];
const { tr } = state;
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 79fc0eea2c7..58fa2655e25 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -50,7 +50,8 @@ export default marks.map((name) =>
},
parseHTML() {
- return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
+ const tag = name === 'span' ? `${name}:not([data-escaped-char])` : name;
+ return [{ tag, priority: PARSE_HTML_PRIORITY_LOWEST }];
},
renderHTML({ HTMLAttributes }) {
diff --git a/app/assets/javascripts/content_editor/extensions/loading.js b/app/assets/javascripts/content_editor/extensions/loading.js
index 0115fb10d5d..942ac650925 100644
--- a/app/assets/javascripts/content_editor/extensions/loading.js
+++ b/app/assets/javascripts/content_editor/extensions/loading.js
@@ -1,4 +1,52 @@
import { Node } from '@tiptap/core';
+import { Decoration, DecorationSet } from '@tiptap/pm/view';
+import { Plugin } from '@tiptap/pm/state';
+
+const createDotsLoader = () => {
+ const root = document.createElement('span');
+ root.classList.add('gl-display-inline-flex', 'gl-align-items-center');
+ root.innerHTML = '<span class="gl-dots-loader gl-mx-2"><span></span></span>';
+ return root;
+};
+
+export const loadingPlugin = new Plugin({
+ state: {
+ init() {
+ return DecorationSet.empty;
+ },
+ apply(tr, set) {
+ let transformedSet = set.map(tr.mapping, tr.doc);
+ const action = tr.getMeta(this);
+
+ if (action?.add) {
+ const deco = Decoration.widget(action.add.pos, createDotsLoader(), {
+ id: action.add.loaderId,
+ side: -1,
+ });
+ transformedSet = transformedSet.add(tr.doc, [deco]);
+ } else if (action?.remove) {
+ transformedSet = transformedSet.remove(
+ transformedSet.find(null, null, (spec) => spec.id === action.remove.loaderId),
+ );
+ }
+ return transformedSet;
+ },
+ },
+ props: {
+ decorations(state) {
+ return this.getState(state);
+ },
+ },
+});
+
+export const findLoader = (state, loaderId) => {
+ const decos = loadingPlugin.getState(state);
+ const found = decos.find(null, null, (spec) => spec.id === loaderId);
+
+ return found.length ? found[0].from : null;
+};
+
+export const findAllLoaders = (state) => loadingPlugin.getState(state).find();
export default Node.create({
name: 'loading',
@@ -13,11 +61,7 @@ export default Node.create({
};
},
- renderHTML() {
- return [
- 'span',
- { class: 'gl-display-inline-flex gl-align-items-center' },
- ['span', { class: 'gl-dots-loader gl-mx-2' }, ['span']],
- ];
+ addProseMirrorPlugins() {
+ return [loadingPlugin];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index f29222a5289..f7ff2fd6647 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -20,6 +20,7 @@ function createSuggestionPlugin({
limit = 15,
nodeType,
nodeProps = {},
+ insertionMap = {},
}) {
const fetchData = memoize(
isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
@@ -36,7 +37,7 @@ function createSuggestionPlugin({
.focus()
.insertContentAt(range, [
{ type: nodeType, attrs: props },
- { type: 'text', text: ' ' },
+ { type: 'text', text: ` ${insertionMap[props.text] || ''}` },
])
.run();
},
@@ -56,6 +57,7 @@ function createSuggestionPlugin({
render: () => {
let component;
let popup;
+ let isHidden = false;
const onUpdate = (props) => {
component?.updateProps({ ...props, loading: false });
@@ -87,6 +89,12 @@ function createSuggestionPlugin({
popup = tippy('body', {
getReferenceClientRect: props.clientRect,
appendTo: () => document.body,
+ onHide: () => {
+ isHidden = true;
+ },
+ onShow: () => {
+ isHidden = false;
+ },
content: component.element,
showOnCreate: true,
interactive: true,
@@ -99,6 +107,8 @@ function createSuggestionPlugin({
onUpdate,
onKeyDown(props) {
+ if (isHidden) return false;
+
if (props.event.key === 'Escape') {
popup?.[0].hide();
@@ -217,11 +227,24 @@ export default Node.create({
referenceType: 'command',
},
search: (query) => ({ name }) => find(name, query),
+ insertionMap: {
+ '/label': '~',
+ '/unlabel': '~',
+ '/relabel': '~',
+ '/assign': '@',
+ '/unassign': '@',
+ '/reassign': '@',
+ '/cc': '@',
+ '/assign_reviewer': '@',
+ '/unassign_reviewer': '@',
+ '/reassign_reviewer': '@',
+ '/milestone': '%',
+ },
}),
createSuggestionPlugin({
editor: this.editor,
char: ':',
- dataSource: () => Object.values(getAllEmoji()),
+ dataSource: () => getAllEmoji(),
nodeType: 'emoji',
search: (query) => ({ d, name }) => find(d, query) || find(name, query),
limit: 10,
diff --git a/app/assets/javascripts/content_editor/extensions/word_break.js b/app/assets/javascripts/content_editor/extensions/word_break.js
index 457b7c36564..01b19cbbd13 100644
--- a/app/assets/javascripts/content_editor/extensions/word_break.js
+++ b/app/assets/javascripts/content_editor/extensions/word_break.js
@@ -24,7 +24,7 @@ export default Node.create({
},
addInputRules() {
- const inputRegex = /^<wbr>$/;
+ const inputRegex = /<wbr>$/;
return [nodeInputRule({ find: inputRegex, type: this.type })];
},
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index bc1ee696323..d3d2d76e481 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -84,9 +84,9 @@ export class ContentEditor {
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor } = this;
- const { doc, tr } = editor.state;
const { document } = await this.deserialize(serializedContent);
+ const { doc, tr } = editor.state;
if (document) {
this._pristineDoc = document;
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
index e3d3360cd0c..3b9f14a218f 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_base.vue
@@ -47,16 +47,20 @@ export default {
<template>
<li class="gl-mt-5 gl-pb-5 gl-border-b gl-relative">
- <time-ago-tooltip :time="event.created_at" class="gl-float-right gl-text-secondary" />
+ <time-ago-tooltip
+ :time="event.created_at"
+ class="gl-float-right gl-font-sm gl-text-secondary gl-mt-2"
+ />
<gl-avatar-link :href="author.web_url">
<gl-avatar-labeled
:label="author.name"
:sub-label="authorUsername"
+ inline-labels
:src="author.avatar_url"
- :size="32"
+ :size="24"
/>
</gl-avatar-link>
- <div class="gl-pl-8 gl-mt-2" data-testid="event-body">
+ <div class="gl-pl-7" data-testid="event-body">
<div class="gl-text-secondary">
<gl-icon :class="iconClass" :name="iconName" />
<gl-sprintf v-if="message" :message="message">
diff --git a/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql b/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql
index a4c46d1f0fa..5ee3da2dfad 100644
--- a/app/assets/javascripts/crm/organizations/components/graphql/update_organization.mutation.graphql
+++ b/app/assets/javascripts/crm/organizations/components/graphql/update_customer_relations_organization.mutation.graphql
@@ -1,6 +1,6 @@
#import "./crm_organization_fields.fragment.graphql"
-mutation updateOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
+mutation updateCustomerRelationsOrganization($input: CustomerRelationsOrganizationUpdateInput!) {
customerRelationsOrganizationUpdate(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 fb056e4fa2c..7dd65205b90 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -5,7 +5,7 @@ import { TYPENAME_CRM_ORGANIZATION, TYPENAME_GROUP } from '~/graphql_shared/cons
import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
import createCustomerRelationsOrganizationMutation from './graphql/create_customer_relations_organization.mutation.graphql';
-import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
+import updateCustomerRelationsOrganizationMutation from './graphql/update_customer_relations_organization.mutation.graphql';
export default {
components: {
@@ -29,7 +29,7 @@ export default {
return convertToGraphQLId(TYPENAME_GROUP, this.groupId);
},
mutation() {
- if (this.isEditMode) return updateOrganizationMutation;
+ if (this.isEditMode) return updateCustomerRelationsOrganizationMutation;
return createCustomerRelationsOrganizationMutation;
},
diff --git a/app/assets/javascripts/custom_emoji/components/delete_item.vue b/app/assets/javascripts/custom_emoji/components/delete_item.vue
index 9d13d40dc47..91bd90c3682 100644
--- a/app/assets/javascripts/custom_emoji/components/delete_item.vue
+++ b/app/assets/javascripts/custom_emoji/components/delete_item.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import { GlButton, GlTooltipDirective, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import deleteCustomEmojiMutation from '../queries/delete_custom_emoji.mutation.graphql';
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index 72d1ce9768a..6210e82119f 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -142,7 +142,6 @@ export default {
ref="freezeStartCron"
v-model="freezeStartCron"
class="gl-font-monospace!"
- data-qa-selector="deploy_freeze_start_field"
:placeholder="$options.i18n.cronPlaceholder"
:state="freezeStartCronState"
autofocus
@@ -160,7 +159,6 @@ export default {
id="deploy-freeze-end"
v-model="freezeEndCron"
class="gl-font-monospace!"
- data-qa-selector="deploy_freeze_end_field"
:placeholder="$options.i18n.cronPlaceholder"
:state="freezeEndCronState"
trim
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 008e12abbcd..9b5b4cef1b9 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -152,7 +152,7 @@ export default class Notes {
// update the file name when an attachment is selected
this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
+ this.$wrapperEl.on('focus', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue
index dec1038d2e3..88c1b444b31 100644
--- a/app/assets/javascripts/design_management/components/delete_button.vue
+++ b/app/assets/javascripts/design_management/components/delete_button.vue
@@ -63,7 +63,7 @@ export default {
title: s__('DesignManagement|Are you sure you want to archive the selected designs?'),
actionPrimary: {
text: s__('DesignManagement|Archive designs'),
- attributes: { variant: 'confirm', 'data-qa-selector': 'confirm_archiving_button' },
+ attributes: { variant: 'confirm', 'data-testid': 'confirm-archiving-button' },
},
actionCancel: {
text: __('Cancel'),
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 45f33967476..2a099b6f22d 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlLink, GlTooltipDirective, GlFormCheckbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -293,7 +293,6 @@ export default {
<ul
class="design-discussion bordered-box gl-relative gl-p-0 gl-list-style-none"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
- data-qa-selector="design_discussion_content"
data-testid="design-discussion-content"
>
<design-note
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index a5b6d6276f8..b247f17fd97 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -7,12 +7,13 @@ import {
GlLink,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { produce } from 'immer';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { __ } from '~/locale';
+import { setUrlFragment } from '~/lib/utils/url_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import EmojiPicker from '~/emoji/components/picker.vue';
@@ -29,6 +30,7 @@ export default {
editCommentLabel: __('Edit comment'),
moreActionsLabel: __('More actions'),
deleteCommentText: __('Delete comment'),
+ copyCommentLink: __('Copy link'),
},
components: {
DesignNoteAwardsList,
@@ -129,19 +131,27 @@ export default {
this.isEditing = true;
},
extraAttrs: {
- 'data-testid': 'delete-note-button',
- 'data-qa-selector': 'delete_design_note_button',
class: 'gl-sm-display-none!',
},
},
{
+ text: this.$options.i18n.copyCommentLink,
+ action: () => {
+ this.$toast.show(__('Link copied to clipboard.'));
+ },
+ extraAttrs: {
+ 'data-clipboard-text': setUrlFragment(
+ window.location.href,
+ `note_${this.noteAnchorId}`,
+ ),
+ },
+ },
+ {
text: this.$options.i18n.deleteCommentText,
action: () => {
this.$emit('delete-note', this.note);
},
extraAttrs: {
- 'data-testid': 'delete-note-button',
- 'data-qa-selector': 'delete_design_note_button',
class: 'gl-text-red-500!',
},
},
@@ -311,7 +321,6 @@ export default {
v-gl-tooltip.hover
icon="ellipsis_v"
category="tertiary"
- data-qa-selector="design_discussion_actions_ellipsis_dropdown"
text-sr-only
:title="$options.i18n.moreActionsLabel"
:aria-label="$options.i18n.moreActionsLabel"
@@ -322,12 +331,7 @@ export default {
</div>
</div>
<template v-if="!isEditing">
- <div
- v-safe-html="note.bodyHtml"
- class="note-text md"
- data-qa-selector="note_content"
- data-testid="note-text"
- ></div>
+ <div v-safe-html="note.bodyHtml" class="note-text md" data-testid="note-text"></div>
<slot name="resolved-status"></slot>
</template>
<design-note-awards-list
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
index f0812e62bba..de3e71c0e9c 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
@@ -37,7 +37,7 @@ export default {
</script>
<template>
- <div class="disabled-comment text-center">
+ <div class="disabled-comment gl-text-center gl-text-secondary">
<gl-sprintf :message="signedOutText">
<template #registerLink="{ content }">
<gl-link :href="registerPath">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 764c78ff581..b6a303ddde8 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -221,7 +221,7 @@ export default {
class="note-textarea js-gfm-input js-autosize markdown-area"
dir="auto"
data-supports-quick-actions="false"
- data-qa-selector="note_textarea"
+ data-testid="note-textarea"
:aria-label="__('Description')"
:placeholder="__('Write a comment…')"
@input="handleInput"
@@ -243,7 +243,7 @@ export default {
variant="confirm"
type="submit"
data-track-action="click_button"
- data-qa-selector="save_comment_button"
+ data-testid="save-comment-button"
@click="submitForm"
>
{{ buttonText }}
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 4ce6395140e..e4361f94026 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -272,7 +272,7 @@ export default {
role="button"
:aria-label="$options.i18n.newCommentButtonLabel"
class="gl-absolute gl-w-full gl-h-full gl-p-0 gl-top-0 gl-left-0 gl-outline-0! btn-transparent gl-hover-cursor-crosshair"
- data-qa-selector="design_image_button"
+ data-testid="design-image-button"
@mouseup="onAddCommentMouseup"
></button>
diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue
index 7b98557f4f0..6400f939244 100644
--- a/app/assets/javascripts/design_management/components/list/item.vue
+++ b/app/assets/javascripts/design_management/components/list/item.vue
@@ -144,12 +144,17 @@ export default {
:name="icon.name"
:size="16"
:class="icon.classes"
- data-qa-selector="design_status_icon"
+ data-testid="design-status-icon"
:data-qa-status="icon.name"
/>
</span>
</div>
- <gl-intersection-observer class="gl-flex-grow-1" @appear="onAppear">
+ <gl-intersection-observer
+ class="gl-flex-grow-1"
+ data-testid="design-image"
+ :data-qa-filename="filename"
+ @appear="onAppear"
+ >
<gl-loading-icon v-if="showLoadingSpinner" size="lg" />
<gl-icon
v-else-if="showImageErrorIcon"
@@ -162,8 +167,6 @@ export default {
:src="imageLink"
:alt="filename"
class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
- data-qa-selector="design_image"
- :data-qa-filename="filename"
:data-testid="`design-img-${id}`"
@load="onImageLoad"
@error="onImageError"
@@ -171,11 +174,13 @@ export default {
</gl-intersection-observer>
</div>
<div class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4">
- <div class="gl-display-flex gl-flex-direction-column str-truncated-100">
+ <div
+ class="gl-display-flex gl-flex-direction-column str-truncated-100"
+ data-testid="design-file-name"
+ >
<span
v-gl-tooltip
class="gl-font-weight-semibold str-truncated-100"
- data-qa-selector="design_file_name"
:data-testid="`design-img-filename-${id}`"
:title="filename"
>{{ filename }}</span
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 09f99f0927f..a1fd3520982 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -402,7 +402,7 @@ export default {
button-variant="default"
button-class="gl-mr-3"
button-size="small"
- data-qa-selector="archive_button"
+ data-testid="archive-button"
:loading="loading"
:has-selected-designs="hasSelectedDesigns"
@delete-selected-designs="mutate()"
@@ -490,7 +490,7 @@ export default {
:checked="isDesignSelected(design.filename)"
type="checkbox"
class="design-checkbox gl-absolute gl-top-4 gl-left-6 gl-ml-2"
- data-qa-selector="design_checkbox"
+ data-testid="design-checkbox"
:data-qa-design="design.filename"
@change="changeSelectedDesigns(design.filename)"
/>
@@ -506,7 +506,7 @@ export default {
:class="{ 'design-list-item': !isDesignListEmpty }"
:display-as-card="hasDesigns"
v-bind="$options.dropzoneProps"
- data-qa-selector="design_dropzone_content"
+ data-testid="design-dropzone-content"
@change="onUploadDesign"
@error="onDesignDropzoneError"
>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 924c515ee2d..54c276c36b1 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -16,7 +17,8 @@ import {
import { createAlert } from '~/alert';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, handleLocationHash } from '~/lib/utils/common_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { Mousetrap } from '~/lib/mousetrap';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -39,8 +41,10 @@ import {
TRACKING_SINGLE_FILE_MODE,
TRACKING_MULTIPLE_FILES_MODE,
EVT_MR_PREPARED,
+ EVT_DISCUSSIONS_ASSIGNED,
} from '../constants';
+import { isCollapsed } from '../utils/diff_file';
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
@@ -55,10 +59,16 @@ 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';
+import getMRCodequalityAndSecurityReports from './graphql/get_mr_codequality_and_security_reports.query.graphql';
+
+export const FINDINGS_STATUS_PARSED = 'PARSED';
+export const FINDINGS_STATUS_ERROR = 'ERROR';
+export const FINDINGS_POLL_INTERVAL = 1000;
export default {
name: 'DiffsApp',
+ FINDINGS_STATUS_PARSED,
+ FINDINGS_STATUS_ERROR,
components: {
DiffsFileTree,
FindingsDrawer,
@@ -100,10 +110,10 @@ export default {
required: false,
default: '',
},
- endpointSast: {
- type: String,
+ sastReportAvailable: {
+ type: Boolean,
required: false,
- default: '',
+ default: false,
},
endpointCodequality: {
type: String,
@@ -135,31 +145,53 @@ export default {
diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
subscribedToVirtualScrollingEvents: false,
+ autoScrolled: false,
+ activeProject: undefined,
};
},
apollo: {
- getMRCodequalityReports: {
- query: getMRCodequalityReports,
+ getMRCodequalityAndSecurityReports: {
+ query: getMRCodequalityAndSecurityReports,
+ pollInterval: FINDINGS_POLL_INTERVAL,
variables() {
return { fullPath: this.projectPath, iid: this.iid };
},
skip() {
- return !this.endpointCodequality || !this.sastReportsInInlineDiff;
+ const codeQualityBoolean = Boolean(this.endpointCodequality);
+
+ return !this.sastReportsInInlineDiff || (!codeQualityBoolean && !this.sastReportAvailable);
},
update(data) {
- if (data?.project?.mergeRequest?.codequalityReportsComparer?.report?.newErrors) {
+ const codeQualityBoolean = Boolean(this.endpointCodequality);
+ const { codequalityReportsComparer, sastReport } = data?.project?.mergeRequest || {};
+
+ this.activeProject = data?.project?.mergeRequest?.project;
+ if (
+ (sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) &&
+ (!codeQualityBoolean || codequalityReportsComparer.status === FINDINGS_STATUS_PARSED)
+ ) {
+ this.getMRCodequalityAndSecurityReportStopPolling(
+ this.$apollo.queries.getMRCodequalityAndSecurityReports,
+ );
+ }
+
+ if (sastReport?.status === FINDINGS_STATUS_ERROR && this.sastReportAvailable) {
+ this.fetchScannerFindingsError();
+ }
+
+ if (codequalityReportsComparer?.report?.newErrors) {
this.$store.commit(
'diffs/SET_CODEQUALITY_DATA',
- sortFindingsByFile(
- data.project.mergeRequest.codequalityReportsComparer.report.newErrors,
- ),
+ sortFindingsByFile(codequalityReportsComparer.report.newErrors),
);
}
+
+ if (sastReport?.report) {
+ this.$store.commit('diffs/SET_SAST_DATA', sastReport.report);
+ }
},
error() {
- createAlert({
- message: __('Something went wrong fetching the CodeQuality Findings. Please try again!'),
- });
+ this.fetchScannerFindingsError();
},
},
},
@@ -304,10 +336,6 @@ export default {
this.setCodequalityEndpoint(this.endpointCodequality);
}
- if (this.endpointSast) {
- this.setSastEndpoint(this.endpointSast);
- }
-
if (this.shouldShow) {
this.fetchData();
}
@@ -355,22 +383,15 @@ export default {
this.adjustView();
this.subscribeToEvents();
- this.unwatchDiscussions = this.$watch(
- () => `${this.flatBlobsList.length}:${this.$store.state.notes.discussions.length}`,
- () => {
- this.setDiscussions();
-
- if (this.$store.state.notes.doneFetchingBatchDiscussions) {
- this.unwatchDiscussions();
- }
- },
- );
-
- this.unwatchRetrievingBatches = this.$watch(
- () => `${this.retrievingBatches}:${this.$store.state.notes.discussions.length}`,
- () => {
- if (!this.retrievingBatches && this.$store.state.notes.discussions.length) {
- this.unwatchRetrievingBatches();
+ this.slowHashHandler = debounce(() => {
+ handleLocationHash();
+ this.autoScrolled = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ this.$watch(
+ () => this.$store.state.notes.discussions.length,
+ (newVal, prevVal) => {
+ if (newVal > prevVal) {
+ this.setDiscussions();
}
},
);
@@ -388,7 +409,6 @@ export default {
...mapActions('diffs', [
'moveToNeighboringCommit',
'setCodequalityEndpoint',
- 'setSastEndpoint',
'fetchDiffFilesMeta',
'fetchDiffFilesBatch',
'fetchFileByFile',
@@ -396,7 +416,6 @@ export default {
'setFileForcedOpen',
'fetchCoverageFiles',
'fetchCodequality',
- 'fetchSast',
'rereadNoteHash',
'startRenderDiffsQueue',
'assignDiscussionsToDiff',
@@ -412,6 +431,11 @@ export default {
closeDrawer() {
this.setDrawer({});
},
+ fetchScannerFindingsError() {
+ createAlert({
+ message: __('Something went wrong fetching the Scanner Findings. Please try again.'),
+ });
+ },
subscribeToEvents() {
notesEventHub.$once('fetchDiffData', this.fetchData);
notesEventHub.$on('refetchDiffData', this.refetchDiffData);
@@ -419,8 +443,13 @@ export default {
diffsEventHub.$on('diffFilesModified', this.setDiscussions);
diffsEventHub.$on('doneLoadingBatches', this.autoScroll);
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
+ diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
+ },
+ getMRCodequalityAndSecurityReportStopPolling(query) {
+ query.stopPolling();
},
unsubscribeFromEvents() {
+ diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
diffsEventHub.$off('doneLoadingBatches', this.autoScroll);
diffsEventHub.$off('diffFilesModified', this.setDiscussions);
@@ -436,15 +465,27 @@ export default {
const idx = this.diffs.findIndex((diffFile) => diffFile.file_hash === sha1InHash);
const file = this.diffs[idx];
+ if (!isCollapsed(file)) return;
+
this.loadCollapsedDiff({ file })
.then(() => {
this.setDiscussions();
- this.scrollVirtualScrollerToIndex(idx);
this.setFileForcedOpen({ filePath: file.new_path });
+
+ this.$nextTick(() => this.scrollVirtualScrollerToIndex(idx));
})
.catch(() => {});
}
},
+ handleHash() {
+ if (this.viewDiffsFileByFile && !this.autoScrolled) {
+ const file = this.diffs[0];
+
+ if (file && !file.isLoadingFullFile) {
+ requestIdleCallback(() => this.slowHashHandler());
+ }
+ }
+ },
navigateToDiffFileNumber(number) {
this.navigateToDiffFileIndex(number - 1);
},
@@ -482,7 +523,7 @@ export default {
})
.catch(() => {
createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ message: __('Something went wrong on our end. Please try again.'),
});
});
}
@@ -499,7 +540,7 @@ export default {
})
.catch(() => {
createAlert({
- message: __('Something went wrong on our end. Please try again!'),
+ message: __('Something went wrong on our end. Please try again.'),
});
});
}
@@ -512,10 +553,6 @@ export default {
this.fetchCodequality();
}
- if (this.endpointSast) {
- this.fetchSast();
- }
-
if (!this.isNotesFetched) {
notesEventHub.$emit('fetchNotesData');
}
@@ -641,7 +678,7 @@ export default {
<template>
<div v-show="shouldShow">
- <findings-drawer :drawer="activeDrawer" @close="closeDrawer" />
+ <findings-drawer :project="activeProject" :drawer="activeDrawer" @close="closeDrawer" />
<div v-if="isLoading || !isTreeLoaded" class="loading"><gl-loading-icon size="lg" /></div>
<div v-else id="diffs" :class="{ active: shouldShow }" class="diffs tab-pane">
<compare-versions :diff-files-count-text="numTotalFiles" />
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 3746ab9427f..7493bd5fdf7 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -135,7 +135,7 @@ export default {
<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">
+ <div class="commit-content" data-testid="commit-content">
<a
v-safe-html:[$options.safeHtmlConfig]="commit.title_html"
:href="commit.commit_url"
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 8915f32eadf..556f72059c2 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -39,12 +39,6 @@ export default {
},
methods: {
...mapActions(['toggleDiscussion']),
- ...mapActions('diffs', ['removeDiscussionsFromDiff']),
- deleteNoteHandler(discussion) {
- if (discussion.notes.length <= 1) {
- this.removeDiscussionsFromDiff(discussion);
- }
- },
isExpanded(discussion) {
return this.shouldCollapseDiscussions ? discussion.expanded : true;
},
@@ -90,7 +84,6 @@ export default {
:line="line"
:help-page-path="helpPagePath"
:should-scroll-to-note="false"
- @noteDeleted="deleteNoteHandler"
>
<template v-if="renderAvatarBadge" #avatar-badge>
<design-note-pin
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index c74a4b47fcb..8c1cab20ece 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -208,11 +208,6 @@ export default {
this.manageViewedEffects();
},
},
- 'file.viewer.forceOpen': {
- handler: function fileForcedOpenHandler() {
- this.handleToggle();
- },
- },
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
if (
diff --git a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql
new file mode 100644
index 00000000000..bd8f408f5a1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_and_security_reports.query.graphql
@@ -0,0 +1,82 @@
+query getMRCodequalityAndSecurityReports($fullPath: ID!, $iid: String!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ title
+ project {
+ id
+ nameWithNamespace
+ fullPath
+ }
+ hasSecurityReports
+ codequalityReportsComparer {
+ status
+ 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
+ }
+ }
+ }
+ sastReport: findingReportsComparer(reportType: SAST) {
+ status
+ report {
+ added {
+ identifiers {
+ externalId
+ externalType
+ name
+ url
+ }
+ uuid
+ title
+ description
+ state
+ severity
+ foundByPipelineIid
+ location {
+ ... on VulnerabilityLocationSast {
+ file
+ startLine
+ endLine
+ vulnerableClass
+ vulnerableMethod
+ blobPath
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
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
deleted file mode 100644
index b6920d0f6ec..00000000000
--- a/app/assets/javascripts/diffs/components/graphql/get_mr_codequality_reports.query.graphql
+++ /dev/null
@@ -1,46 +0,0 @@
-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/shared/findings_drawer.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
index fddd455b17e..2c1a8305935 100644
--- a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
@@ -1,46 +1,56 @@
<script>
-import { GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { s__ } from '~/locale';
+import { GlBadge, GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { getSeverity } from '~/ci/reports/utils';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import DrawerItem from './findings_drawer_item.vue';
export const i18n = {
- severity: s__('FindingsDrawer|Severity:'),
- engine: s__('FindingsDrawer|Engine:'),
- category: s__('FindingsDrawer|Category:'),
- otherLocations: s__('FindingsDrawer|Other locations:'),
+ name: __('Name'),
+ description: __('Description'),
+ status: __('Status'),
+ sast: __('SAST'),
+ engine: __('Engine'),
+ identifiers: __('Identifiers'),
+ project: __('Project'),
+ file: __('File'),
+ tool: __('Tool'),
+ codeQualityFinding: s__('FindingsDrawer|Code Quality Finding'),
+ sastFinding: s__('FindingsDrawer|SAST Finding'),
+ codeQuality: s__('FindingsDrawer|Code Quality'),
+ detected: s__('FindingsDrawer|Detected in pipeline'),
};
+export const codeQuality = 'codeQuality';
export default {
i18n,
- components: { GlDrawer, GlIcon, GlLink },
- directives: {
- SafeHtml,
- },
+ codeQuality,
+ components: { GlBadge, GlDrawer, GlIcon, GlLink, DrawerItem },
props: {
drawer: {
type: Object,
required: true,
},
- },
- safeHtmlConfig: {
- ALLOWED_TAGS: ['a', 'h1', 'h2', 'p'],
- ALLOWED_ATTR: ['href', 'rel'],
+ project: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
+ isCodeQuality() {
+ return this.drawer.scale === this.$options.codeQuality;
+ },
},
DRAWER_Z_INDEX,
methods: {
- severityClass(severity) {
- return SEVERITY_CLASSES[severity] || SEVERITY_CLASSES.unknown;
- },
- severityIcon(severity) {
- return SEVERITY_ICONS[severity] || SEVERITY_ICONS.unknown;
+ getSeverity,
+ concatIdentifierName(name, index) {
+ return name + (index !== this.drawer.identifiers.length - 1 ? ', ' : '');
},
},
};
@@ -54,57 +64,82 @@ export default {
@close="$emit('close')"
>
<template #title>
- <h2 data-testid="findings-drawer-heading" class="gl-font-size-h2 gl-mt-0 gl-mb-0">
- {{ drawer.description }}
+ <h2 class="drawer-heading gl-font-base gl-mt-0 gl-mb-0">
+ <gl-icon
+ :size="12"
+ :name="getSeverity(drawer).name"
+ :class="getSeverity(drawer).class"
+ class="inline-findings-severity-icon gl-vertical-align-baseline!"
+ />
+ <span class="drawer-heading-severity">{{ drawer.severity }}</span>
+ {{ isCodeQuality ? $options.i18n.codeQualityFinding : $options.i18n.sastFinding }}
</h2>
</template>
<template #default>
<ul class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!">
- <li data-testid="findings-drawer-severity" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.severity }}</span>
- <gl-icon
- data-testid="findings-drawer-severity-icon"
- :size="12"
- :name="severityIcon(drawer.severity)"
- :class="severityClass(drawer.severity)"
- class="inline-findings-severity-icon"
- />
+ <drawer-item v-if="drawer.title" :description="$options.i18n.name" :value="drawer.title" />
+
+ <drawer-item v-if="drawer.state" :description="$options.i18n.status">
+ <template #value>
+ <gl-badge variant="warning" class="text-capitalize">{{ drawer.state }}</gl-badge>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.description"
+ :description="$options.i18n.description"
+ :value="drawer.description"
+ />
+
+ <drawer-item
+ v-if="project && drawer.scale !== $options.codeQuality"
+ :description="$options.i18n.project"
+ >
+ <template #value>
+ <gl-link :href="`/${project.fullPath}`">{{ project.nameWithNamespace }}</gl-link>
+ </template>
+ </drawer-item>
+
+ <drawer-item v-if="drawer.location || drawer.webUrl" :description="$options.i18n.file">
+ <template #value>
+ <span v-if="drawer.webUrl && drawer.filePath && drawer.line">
+ <gl-link :href="drawer.webUrl">{{ drawer.filePath }}:{{ drawer.line }}</gl-link>
+ </span>
+ <span v-else-if="drawer.location">
+ {{ drawer.location.file }}:{{ drawer.location.startLine }}
+ </span>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.identifiers && drawer.identifiers.length"
+ :description="$options.i18n.identifiers"
+ >
+ <template #value>
+ <span v-for="(identifier, index) in drawer.identifiers" :key="identifier.externalId">
+ <gl-link v-if="identifier.url" :href="identifier.url">
+ {{ concatIdentifierName(identifier.name, index) }}
+ </gl-link>
+ <span v-else>
+ {{ concatIdentifierName(identifier.name, index) }}
+ </span>
+ </span>
+ </template>
+ </drawer-item>
+
+ <drawer-item
+ v-if="drawer.scale"
+ :description="$options.i18n.tool"
+ :value="isCodeQuality ? $options.i18n.codeQuality : $options.i18n.sast"
+ />
- {{ drawer.severity }}
- </li>
- <li data-testid="findings-drawer-engine" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.engine }}</span>
- {{ drawer.engineName }}
- </li>
- <li data-testid="findings-drawer-category" class="gl-mb-4">
- <span class="gl-font-weight-bold">{{ $options.i18n.category }}</span>
- {{ drawer.categories ? drawer.categories[0] : '' }}
- </li>
- <li data-testid="findings-drawer-other-locations" class="gl-mb-4">
- <span class="gl-font-weight-bold gl-mb-3 gl-display-block">{{
- $options.i18n.otherLocations
- }}</span>
- <ul class="gl-pl-6">
- <li
- v-for="otherLocation in drawer.otherLocations"
- :key="otherLocation.path"
- class="gl-mb-1"
- >
- <gl-link
- data-testid="findings-drawer-other-locations-link"
- :href="otherLocation.href"
- >{{ otherLocation.path }}</gl-link
- >
- </li>
- </ul>
- </li>
+ <drawer-item
+ v-if="drawer.engineName"
+ :description="$options.i18n.engine"
+ :value="drawer.engineName"
+ />
</ul>
- <span
- v-safe-html:[$options.safeHtmlConfig]="drawer.content ? drawer.content.body : ''"
- data-testid="findings-drawer-body"
- class="drawer-body gl-display-block gl-px-3 gl-py-0!"
- ></span>
</template>
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue
new file mode 100644
index 00000000000..f488e8e3bb1
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer_item.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <li class="gl-mb-4">
+ <p class="gl-line-height-20">
+ <span
+ data-testid="findings-drawer-item-description"
+ class="gl-font-weight-bold gl-display-block gl-mb-1"
+ >{{ description }}</span
+ >
+ <slot name="value">
+ <span data-testid="findings-drawer-item-value-prop">{{ value }}</span>
+ </slot>
+ </p>
+ </li>
+</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index f4715c591b2..07984beb709 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -3,22 +3,20 @@ import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import micromatch from 'micromatch';
-import { debounce } from 'lodash';
import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import { RecycleScroller } from 'vendor/vue-virtual-scroller';
-import { contentTop } from '~/lib/utils/common_utils';
import DiffFileRow from './diff_file_row.vue';
+import TreeListHeight from './tree_list_height.vue';
const MODIFIER_KEY = getModifierKey();
-const MAX_ITEMS_ON_NARROW_SCREEN = 8;
-const BOTTOM_MARGIN = 16;
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
+ TreeListHeight,
GlIcon,
DiffFileRow,
RecycleScroller,
@@ -32,17 +30,10 @@ export default {
data() {
return {
search: '',
- scrollerHeight: 0,
- rowHeight: 0,
- debouncedHeightCalc: null,
- reviewBarHeight: 0,
- largeBreakpointSize: 0,
};
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
- ...mapState('batchComments', ['reviewBarRendered']),
- ...mapGetters('batchComments', ['draftsCount']),
...mapGetters('diffs', ['allBlobs']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@@ -95,76 +86,21 @@ export default {
return result;
},
- reviewBarEnabled() {
- return this.draftsCount > 0;
- },
- },
- watch: {
- reviewBarEnabled() {
- this.debouncedHeightCalc();
- },
- calculateReviewBarHeight() {
- this.debouncedHeightCalc();
- },
- },
- created() {
- this.debouncedHeightCalc = debounce(this.calculateScrollerHeight, 50);
- },
- mounted() {
- const heightProp = getComputedStyle(this.$refs.wrapper).getPropertyValue('--file-row-height');
- const breakpointProp = getComputedStyle(window.document.body).getPropertyValue(
- '--breakpoint-lg',
- );
- this.largeBreakpointSize = parseInt(breakpointProp, 10);
- this.rowHeight = parseInt(heightProp, 10);
- this.calculateScrollerHeight();
- let stop;
- // eslint-disable-next-line prefer-const
- stop = this.$watch(
- () => this.reviewBarRendered,
- (enabled) => {
- if (!enabled) return;
- this.calculateReviewBarHeight();
- stop();
- },
- { immediate: true },
- );
- window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
- },
- beforeDestroy() {
- window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
clearSearch() {
this.search = '';
},
- calculateScrollerHeight() {
- if (window.matchMedia(`(max-width: ${this.largeBreakpointSize - 1}px)`).matches) {
- this.calculateMobileScrollerHeight();
- } else {
- let clipping = BOTTOM_MARGIN;
- if (this.reviewBarEnabled) clipping += this.reviewBarHeight;
- this.scrollerHeight = this.$refs.scrollRoot.clientHeight - clipping;
- }
- },
- calculateMobileScrollerHeight() {
- const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.flatFilteredTreeList.length);
- this.scrollerHeight = Math.min(maxItems * this.rowHeight, window.innerHeight - contentTop());
- },
- calculateReviewBarHeight() {
- this.reviewBarHeight = document.querySelector('.js-review-bar')?.offsetHeight || 0;
- },
},
searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)'), {
MODIFIER_KEY,
}),
- DiffFileRow,
};
</script>
<template>
- <div ref="wrapper" class="tree-list-holder d-flex flex-column" data-testid="file-tree-container">
+ <div 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" />
@@ -189,41 +125,41 @@ export default {
</button>
</div>
</div>
- <div
- ref="scrollRoot"
- :class="{ 'tree-list-blobs': !renderTreeList || search }"
- class="gl-flex-grow-1 mr-tree-list"
- >
- <recycle-scroller
- v-if="flatFilteredTreeList.length"
- :style="{ height: `${scrollerHeight}px` }"
- :items="flatFilteredTreeList"
- :item-size="rowHeight"
- :buffer="100"
- key-field="key"
- >
- <template #default="{ item }">
- <diff-file-row
- :file="item"
- :level="item.level"
- :viewed-files="viewedDiffFileIds"
- :hide-file-stats="hideFileStats"
- :current-diff-file-id="currentDiffFileId"
- :style="{ '--level': item.level }"
- :class="{ 'tree-list-parent': item.level > 0 }"
- class="gl-relative"
- @toggleTreeOpen="toggleTreeOpen"
- @clickFile="(path) => goToFile({ path })"
- />
- </template>
- <template #after>
- <div class="tree-list-gutter"></div>
- </template>
- </recycle-scroller>
- <p v-else class="prepend-top-20 append-bottom-20 text-center">
- {{ s__('MergeRequest|No files found') }}
- </p>
- </div>
+ <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length">
+ <template #default="{ scrollerHeight, rowHeight }">
+ <div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
+ <recycle-scroller
+ v-if="flatFilteredTreeList.length"
+ :style="{ height: `${scrollerHeight}px` }"
+ :items="flatFilteredTreeList"
+ :item-size="rowHeight"
+ :buffer="100"
+ key-field="key"
+ >
+ <template #default="{ item }">
+ <diff-file-row
+ :file="item"
+ :level="item.level"
+ :viewed-files="viewedDiffFileIds"
+ :hide-file-stats="hideFileStats"
+ :current-diff-file-id="currentDiffFileId"
+ :style="{ '--level': item.level }"
+ :class="{ 'tree-list-parent': item.level > 0 }"
+ class="gl-relative"
+ @toggleTreeOpen="toggleTreeOpen"
+ @clickFile="(path) => goToFile({ path })"
+ />
+ </template>
+ <template #after>
+ <div class="tree-list-gutter"></div>
+ </template>
+ </recycle-scroller>
+ <p v-else class="prepend-top-20 append-bottom-20 text-center">
+ {{ s__('MergeRequest|No files found') }}
+ </p>
+ </div>
+ </template>
+ </tree-list-height>
</div>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list_height.vue b/app/assets/javascripts/diffs/components/tree_list_height.vue
new file mode 100644
index 00000000000..4da94cacd75
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/tree_list_height.vue
@@ -0,0 +1,108 @@
+<script>
+import { debounce } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import { mapState, mapGetters } from 'vuex';
+import { contentTop } from '~/lib/utils/common_utils';
+
+const MAX_ITEMS_ON_NARROW_SCREEN = 8;
+// Should be enough for the very long titles (10+ lines) on the max smallest screen
+const MAX_SCROLL_Y = 600;
+const BOTTOM_OFFSET = 16;
+
+export default {
+ name: 'TreeListHeight',
+ props: {
+ itemsCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollerHeight: 0,
+ rowHeight: 0,
+ reviewBarHeight: 0,
+ scrollY: 0,
+ isNarrowScreen: false,
+ mediaQueryMatch: null,
+ };
+ },
+ computed: {
+ ...mapState('batchComments', ['reviewBarRendered']),
+ ...mapGetters('batchComments', ['draftsCount']),
+ reviewBarEnabled() {
+ return this.draftsCount > 0;
+ },
+ debouncedHeightCalc() {
+ return debounce(this.calculateScrollerHeight, 100);
+ },
+ debouncedRecordScroll() {
+ return debounce(this.recordScroll, 50);
+ },
+ },
+ watch: {
+ reviewBarRendered: {
+ handler(rendered) {
+ if (!rendered || this.reviewBarHeight) return;
+ this.reviewBarHeight = document.querySelector('.js-review-bar').offsetHeight;
+ this.debouncedHeightCalc();
+ },
+ immediate: true,
+ },
+ reviewBarEnabled: 'debouncedHeightCalc',
+ scrollY: 'debouncedHeightCalc',
+ isNarrowScreen: 'recordScroll',
+ },
+ mounted() {
+ const computedStyles = getComputedStyle(this.$refs.scrollRoot);
+ this.rowHeight = parseInt(computedStyles.getPropertyValue('--file-row-height'), 10);
+
+ const largeBreakpointSize = parseInt(computedStyles.getPropertyValue('--breakpoint-lg'), 10);
+ this.mediaQueryMatch = window.matchMedia(`(max-width: ${largeBreakpointSize - 1}px)`);
+ this.isNarrowScreen = this.mediaQueryMatch.matches;
+ this.mediaQueryMatch.addEventListener('change', this.handleMediaMatch);
+
+ window.addEventListener('resize', this.debouncedHeightCalc, { passive: true });
+ window.addEventListener('scroll', this.debouncedRecordScroll, { passive: true });
+
+ this.calculateScrollerHeight();
+ },
+ beforeDestroy() {
+ this.mediaQueryMatch.removeEventListener('change', this.handleMediaMatch);
+ this.mediaQueryMatch = null;
+ window.removeEventListener('resize', this.debouncedHeightCalc, { passive: true });
+ window.removeEventListener('scroll', this.debouncedRecordScroll, { passive: true });
+ },
+ methods: {
+ recordScroll() {
+ const { scrollY } = window;
+ if (scrollY > MAX_SCROLL_Y || this.isNarrowScreen) {
+ this.scrollY = MAX_SCROLL_Y;
+ } else {
+ this.scrollY = window.scrollY;
+ }
+ },
+ handleMediaMatch({ matches }) {
+ this.isNarrowScreen = matches;
+ },
+ calculateScrollerHeight() {
+ if (this.isNarrowScreen) {
+ const maxItems = Math.min(MAX_ITEMS_ON_NARROW_SCREEN, this.itemsCount);
+ const maxHeight = maxItems * this.rowHeight;
+ this.scrollerHeight = Math.min(maxHeight, window.innerHeight - contentTop());
+ } else {
+ const { y } = this.$refs.scrollRoot.getBoundingClientRect();
+ const reviewBarOffset = this.reviewBarEnabled ? this.reviewBarHeight : 0;
+ // distance from element's top vertical position in the viewport to the bottom of the viewport minus offsets
+ this.scrollerHeight = window.innerHeight - y - reviewBarOffset - BOTTOM_OFFSET;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div ref="scrollRoot">
+ <slot :scroller-height="scrollerHeight" :row-height="rowHeight"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 575cd05ceb8..e48eb10753c 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -82,6 +82,7 @@ export const RENAMED_DIFF_TRANSITIONS = {
// MR Diffs known events
export const EVT_MR_PREPARED = 'mr:asyncPreparationFinished';
export const EVT_EXPAND_ALL_FILES = 'mr:diffs:expandAllFiles';
+export const EVT_DISCUSSIONS_ASSIGNED = 'mr:diffs:discussionsAssigned';
export const EVT_PERF_MARK_FILE_TREE_START = 'mr:diffs:perf:fileTreeStart';
export const EVT_PERF_MARK_FILE_TREE_END = 'mr:diffs:perf:fileTreeEnd';
export const EVT_PERF_MARK_DIFF_FILES_START = 'mr:diffs:perf:filesStart';
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index c0b6c8159dc..034dd4cf6d2 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -36,7 +36,7 @@ export default function initDiffsApp(store = notesStore) {
iid: dataset.iid || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
- endpointSast: dataset.endpointSast || '',
+ sastReportAvailable: dataset.endpointSast,
helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
@@ -86,7 +86,7 @@ export default function initDiffsApp(store = notesStore) {
iid: this.iid,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
- endpointSast: this.endpointSast,
+ sastReportAvailable: this.sastReportAvailable,
currentUser: this.currentUser,
helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index ed8ae795bda..fcaf8e99b2d 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -49,6 +49,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
EVT_MR_PREPARED,
FILE_DIFF_POSITION_TYPE,
+ EVT_DISCUSSIONS_ASSIGNED,
} from '../constants';
import {
DISCUSSION_SINGLE_DIFF_FAILED,
@@ -89,6 +90,7 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
} = options;
commit(types.SET_BASE_CONFIG, {
endpoint,
@@ -104,6 +106,7 @@ export const setBaseConfig = ({ commit }, options) => {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
});
Array.from(new Set(Object.values(mrReviews).flat())).forEach((id) => {
@@ -206,7 +209,7 @@ export const fetchFileByFile = async ({ state, getters, commit }) => {
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
- let perPage = state.viewDiffsFileByFile ? 1 : 5;
+ let perPage = state.viewDiffsFileByFile ? 1 : state.perPage;
let increaseAmount = 1.4;
const startPage = 0;
const id = window?.location?.hash;
@@ -413,12 +416,16 @@ export const assignDiscussionsToDiff = (
}
Vue.nextTick(() => {
- notesEventHub.$emit('scrollToDiscussion');
+ eventHub.$emit(EVT_DISCUSSIONS_ASSIGNED);
});
};
export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
- const { file_hash: fileHash, line_code: lineCode, id } = removeDiscussion;
+ const {
+ diff_file: { file_hash: fileHash },
+ line_code: lineCode,
+ id,
+ } = removeDiscussion;
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode, id });
};
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 31369b169f5..08c195469e3 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -39,6 +39,7 @@ export default {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
} = options;
Object.assign(state, {
endpoint,
@@ -54,6 +55,7 @@ export default {
viewDiffsFileByFile,
mrReviews,
diffViewType,
+ perPage,
});
},
@@ -198,9 +200,10 @@ export default {
return {
...line,
discussionsExpanded:
- line.discussions && line.discussions.length
+ line.discussionsExpanded ||
+ (line.discussions && line.discussions.length
? line.discussions.some((disc) => !disc.resolved) || isLineNoteTargeted
- : false,
+ : false),
};
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 15d2ab71bc8..fb467a606b9 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -338,7 +338,7 @@ function prepareLine(line, file) {
problems.brokenSymlink || problems.fileOnlyMoved || problems.brokenLineCode,
),
rich_text: cleanRichText(line.rich_text),
- discussionsExpanded: true,
+ discussionsExpanded: false,
discussions: [],
hasForm: false,
text: undefined,
diff --git a/app/assets/javascripts/diffs/utils/file_reviews.js b/app/assets/javascripts/diffs/utils/file_reviews.js
index 227be4e4a6c..581d0b6055b 100644
--- a/app/assets/javascripts/diffs/utils/file_reviews.js
+++ b/app/assets/javascripts/diffs/utils/file_reviews.js
@@ -43,7 +43,7 @@ export function reviewable(file) {
}
export function markFileReview(reviews, file, reviewed = true) {
- const usableReviews = { ...(reviews || {}) };
+ const usableReviews = { ...reviews };
const updatedReviews = usableReviews;
let fileReviews;
diff --git a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
index 3a285e80ace..3cf6dc169e4 100644
--- a/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
+++ b/app/assets/javascripts/diffs/utils/sort_findings_by_file.js
@@ -1,10 +1,17 @@
export function sortFindingsByFile(newErrors = []) {
const files = {};
- newErrors.forEach(({ filePath, line, description, severity }) => {
+ newErrors.forEach(({ line, description, severity, filePath, webUrl, engineName }) => {
if (!files[filePath]) {
files[filePath] = [];
}
- files[filePath].push({ line, description, severity: severity.toLowerCase() });
+ files[filePath].push({
+ line,
+ description,
+ severity: severity.toLowerCase(),
+ filePath,
+ webUrl,
+ engineName,
+ });
});
const sortedFiles = Object.keys(files)
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 2be671ec7d8..2d50b7e4319 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -53,11 +53,6 @@ export const EDITOR_EXTENSION_STORE_IS_MISSING_ERROR = s__(
export const EXTENSION_BASE_LINE_LINK_ANCHOR_CLASS = 'link-anchor';
export const EXTENSION_BASE_LINE_NUMBERS_CLASS = 'line-numbers';
-// For CI config schemas the filename must match
-// '*.gitlab-ci.yml' regardless of project configuration.
-// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
-export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
-
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_CLASS = 'md';
export const EXTENSION_MARKDOWN_PREVIEW_PANEL_PARENT_CLASS = 'source-editor-preview';
export const EXTENSION_MARKDOWN_PREVIEW_ACTION_ID = 'markdown-preview';
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 0420ffb82f5..308a68544bc 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -2093,9 +2093,15 @@
"description": "A path to a directory that contains the files to be published with Pages",
"type": "string"
},
- "pages_path_prefix": {
- "description": "The path prefix identifier for this version of pages. Allows creation of multiple versions of the same site with different path prefixes",
- "type": "string"
+ "pages": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "path_prefix": {
+ "type": "string",
+ "markdownDescription": "The GitLab Pages URL path prefix used in this version of pages."
+ }
+ }
}
},
"oneOf": [
diff --git a/app/assets/javascripts/emoji/awards_app/store/actions.js b/app/assets/javascripts/emoji/awards_app/store/actions.js
index 677c11277a3..54d98687684 100644
--- a/app/assets/javascripts/emoji/awards_app/store/actions.js
+++ b/app/assets/javascripts/emoji/awards_app/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 238f0d81b22..462420ba4e5 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -3,8 +3,8 @@
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';
-import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
+import { getEmojiCategoryMap, state } from '~/emoji';
+import { CATEGORY_NAMES, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from '../constants';
import Category from './category.vue';
import EmojiList from './emoji_list.vue';
import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from './utils';
diff --git a/app/assets/javascripts/emoji/constants.js b/app/assets/javascripts/emoji/constants.js
index 215ecbfe605..c8bcb79ad15 100644
--- a/app/assets/javascripts/emoji/constants.js
+++ b/app/assets/javascripts/emoji/constants.js
@@ -1,6 +1,18 @@
export const FREQUENTLY_USED_KEY = 'frequently_used';
export const FREQUENTLY_USED_COOKIE_KEY = 'frequently_used_emojis';
+export const CATEGORY_NAMES = [
+ FREQUENTLY_USED_KEY,
+ 'custom',
+ 'people',
+ 'activity',
+ 'nature',
+ 'food',
+ 'travel',
+ 'objects',
+ 'symbols',
+ 'flags',
+];
export const CATEGORY_ICON_MAP = {
[FREQUENTLY_USED_KEY]: 'history',
custom: 'tanuki',
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 1fa81a000a5..f98369c2fde 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -8,7 +8,7 @@ import { getEmojiScoreWithIntent } from '~/emoji/utils';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import customEmojiQuery from './queries/custom_emoji.query.graphql';
-import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
+import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_NAMES, FREQUENTLY_USED_KEY } from './constants';
let emojiMap = null;
let validEmojiNames = null;
@@ -20,22 +20,27 @@ export const state = Vue.observable({
export const FALLBACK_EMOJI_KEY = 'grey_question';
// Keep the version in sync with `lib/gitlab/emoji.rb`
-export const EMOJI_VERSION = '2';
+export const EMOJI_VERSION = '3';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
- if (
- isLocalStorageAvailable &&
- window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
- window.localStorage.getItem(CACHE_KEY)
- ) {
- const emojis = JSON.parse(window.localStorage.getItem(CACHE_KEY));
- // Workaround because the pride flag is broken in EMOJI_VERSION = '1'
- if (emojis.gay_pride_flag) {
- emojis.gay_pride_flag.e = '🏳️‍🌈';
+ try {
+ window.localStorage.removeItem(CACHE_VERSION_KEY);
+ } catch {
+ // Cleanup after us and remove the old EMOJI_VERSION_KEY
+ }
+
+ try {
+ if (isLocalStorageAvailable) {
+ const parsed = JSON.parse(window.localStorage.getItem(CACHE_KEY));
+ if (parsed?.EMOJI_VERSION === EMOJI_VERSION && parsed.data) {
+ return parsed.data;
+ }
}
- return emojis;
+ } catch {
+ // Maybe the stored data was corrupted or the version didn't match.
+ // Let's not error out.
}
// We load the JSON file direct from the server
@@ -44,21 +49,31 @@ async function loadEmoji() {
const { data } = await axios.get(
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
);
- window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
- window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
+
+ try {
+ window.localStorage.setItem(CACHE_KEY, JSON.stringify({ data, EMOJI_VERSION }));
+ } catch {
+ // Setting data in localstorage may fail when storage quota is exceeded.
+ // We should continue even when this fails.
+ }
+
return data;
}
async function loadEmojiWithNames() {
const emojiRegex = emojiRegexFactory();
- return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
- // Filter out entries which aren't emojis
- if (value.e.match(emojiRegex)?.[0] === value.e) {
- acc[key] = { ...value, name: key };
- }
- return acc;
- }, {});
+ return (await loadEmoji()).reduce(
+ (acc, emoji) => {
+ // Filter out entries which aren't emojis
+ if (emoji.e.match(emojiRegex)?.[0] === emoji.e) {
+ acc.emojis[emoji.n] = { ...emoji, name: emoji.n };
+ acc.names.push(emoji.n);
+ }
+ return acc;
+ },
+ { emojis: {}, names: [] },
+ );
}
export async function loadCustomEmojiWithNames() {
@@ -71,31 +86,35 @@ export async function loadCustomEmojiWithNames() {
},
});
- return data?.group?.customEmoji?.nodes?.reduce((acc, e) => {
- // Map the custom emoji into the format of the normal emojis
- acc[e.name] = {
- c: 'custom',
- d: e.name,
- e: undefined,
- name: e.name,
- src: e.url,
- u: 'custom',
- };
+ return data?.group?.customEmoji?.nodes?.reduce(
+ (acc, e) => {
+ // Map the custom emoji into the format of the normal emojis
+ acc.emojis[e.name] = {
+ c: 'custom',
+ d: e.name,
+ e: undefined,
+ name: e.name,
+ src: e.url,
+ u: 'custom',
+ };
+ acc.names.push(e.name);
- return acc;
- }, {});
+ return acc;
+ },
+ { emojis: {}, names: [] },
+ );
}
- return {};
+ return { emojis: {}, names: [] };
}
async function prepareEmojiMap() {
return Promise.all([loadEmojiWithNames(), loadCustomEmojiWithNames()]).then((values) => {
emojiMap = {
- ...values[0],
- ...values[1],
+ ...values[0].emojis,
+ ...values[1].emojis,
};
- validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)];
+ validEmojiNames = [...values[0].names, ...values[1].names];
state.loading = false;
});
}
@@ -109,10 +128,6 @@ export function normalizeEmojiName(name) {
return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name;
}
-export function getValidEmojiNames() {
- return validEmojiNames;
-}
-
export function isEmojiNameValid(name) {
if (!emojiMap) {
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -122,10 +137,14 @@ export function isEmojiNameValid(name) {
return name in emojiMap || name in emojiAliases;
}
-export function getAllEmoji() {
+export function getEmojiMap() {
return emojiMap;
}
+export function getAllEmoji() {
+ return validEmojiNames.map((n) => emojiMap[n]);
+}
+
export function findCustomEmoji(name) {
return emojiMap[name];
}
@@ -218,8 +237,6 @@ export function searchEmoji(query) {
.sort(sortEmoji);
}
-export const CATEGORY_NAMES = Object.keys(CATEGORY_ICON_MAP);
-
let emojiCategoryMap;
export function getEmojiCategoryMap() {
if (!emojiCategoryMap && emojiMap) {
@@ -229,7 +246,7 @@ export function getEmojiCategoryMap() {
}
return { ...acc, [category]: [] };
}, {});
- Object.keys(emojiMap).forEach((name) => {
+ validEmojiNames.forEach((name) => {
const emoji = emojiMap[name];
if (emojiCategoryMap[emoji.c]) {
emojiCategoryMap[emoji.c].push(name);
diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js
index 4566ab20258..ddb34e59144 100644
--- a/app/assets/javascripts/ensure_data.js
+++ b/app/assets/javascripts/ensure_data.js
@@ -1,6 +1,6 @@
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg?raw';
import { GlEmptyState } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
export const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js
index 8eb265cb1e8..e18c4bc8742 100644
--- a/app/assets/javascripts/entrypoints/analytics.js
+++ b/app/assets/javascripts/entrypoints/analytics.js
@@ -14,4 +14,10 @@ if (appId && host) {
errorTracking: false,
},
});
+
+ const userId = window.gl?.snowplowStandardContext?.data?.user_id;
+
+ if (userId) {
+ window.glClient?.identify(userId);
+ }
}
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 2cf71de7ea2..2bc65e4ad04 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -3,8 +3,8 @@
* Render modal to confirm rollback/redeploy.
*/
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { escape } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 8ebba0e27bb..c6cf6b7e24b 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -193,6 +193,7 @@ export default {
headers: {
'GitLab-Agent-Id': getIdFromGraphQLId(this.selectedAgentId),
'Content-Type': 'application/json',
+ Accept: 'application/json',
...csrf.headers,
},
credentials: 'include',
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 08a1eacec7a..47edec8dcb0 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -752,7 +752,7 @@ export default {
:title="upcomingDeploymentTooltipText"
data-testid="upcoming-deployment-status-link"
>
- <ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" />
+ <ci-icon :status="upcomingDeployment.deployable.status" class="gl-mr-2" />
</gl-link>
</div>
<span
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 795cbf5327a..4e8b75536a4 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -78,7 +78,7 @@ export default {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review apps'),
cleanUpEnvsButtonLabel: s__('Environments|Clean up environments'),
- available: __('Available'),
+ active: __('Active'),
stopped: __('Stopped'),
prevPage: __('Go to previous page'),
nextPage: __('Go to next page'),
@@ -97,9 +97,7 @@ export default {
isStopStaleEnvModalVisible: false,
page: parseInt(page, 10),
pageInfo: {},
- scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope)
- ? scope
- : ENVIRONMENTS_SCOPE.AVAILABLE,
+ scope: Object.values(ENVIRONMENTS_SCOPE).includes(scope) ? scope : ENVIRONMENTS_SCOPE.ACTIVE,
environmentToDelete: {},
environmentToRollback: {},
environmentToStop: {},
@@ -112,6 +110,9 @@ export default {
canSetupReviewApp() {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
+ hasReviewApp() {
+ return this.environmentApp?.reviewApp?.hasReviewApp;
+ },
canCleanUpEnvs() {
return this.environmentApp?.canStopStaleEnvironments;
},
@@ -130,14 +131,14 @@ export default {
hasSearch() {
return Boolean(this.search);
},
- availableCount() {
- return this.environmentApp?.availableCount;
+ activeCount() {
+ return this.environmentApp?.activeCount ?? 0;
},
stoppedCount() {
- return this.environmentApp?.stoppedCount;
+ return this.environmentApp?.stoppedCount ?? 0;
},
hasAnyEnvironment() {
- return this.availableCount > 0 || this.stoppedCount > 0;
+ return this.activeCount > 0 || this.stoppedCount > 0;
},
showContent() {
return this.hasAnyEnvironment || this.hasSearch;
@@ -157,7 +158,10 @@ export default {
};
},
openReviewAppModal() {
- if (!this.canSetupReviewApp) {
+ // we don't show the Enable review apps button
+ // if a user cannot setup a review app or review
+ // apps are already configured
+ if (!this.canSetupReviewApp || this.hasReviewApp) {
return null;
}
@@ -272,13 +276,13 @@ export default {
@primary="showCleanUpEnvsModal"
>
<gl-tab
- :query-param-value="$options.ENVIRONMENTS_SCOPE.AVAILABLE"
- @click="setScope($options.ENVIRONMENTS_SCOPE.AVAILABLE)"
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)"
>
<template #title>
- <span>{{ $options.i18n.available }}</span>
+ <span>{{ $options.i18n.active }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">
- {{ availableCount }}
+ {{ activeCount }}
</gl-badge>
</template>
</gl-tab>
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index b47086a19da..2f49ed847bf 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -147,12 +147,7 @@ export default {
</div>
</div>
<template v-for="(model, i) in sortedEnvironments">
- <environment-item
- :key="`environment-item-${i}`"
- :model="model"
- :table-data="tableData"
- data-qa-selector="environment_item"
- />
+ <environment-item :key="`environment-item-${i}`" :model="model" :table-data="tableData" />
<div
v-if="shouldRenderDeployBoard(model)"
@@ -185,7 +180,6 @@ export default {
:key="`environment-row-${i}-${index}`"
:model="child"
:table-data="tableData"
- data-qa-selector="environment_item"
/>
<div
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 252ced6391d..36cce29d624 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -64,6 +64,7 @@ export default {
headers: {
'GitLab-Agent-Id': this.gitlabAgentId,
'Content-Type': 'application/json',
+ Accept: 'application/json',
...csrf.headers,
},
credentials: 'include',
@@ -110,7 +111,6 @@ export default {
<kubernetes-status-bar
:cluster-health-status="clusterHealthStatus"
:configuration="k8sAccessConfiguration"
- :namespace="namespace"
:environment-name="environmentName"
:flux-resource-path="fluxResourcePath"
class="gl-mb-3" />
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index c603d83db9c..8ecb61711ce 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -37,11 +37,6 @@ export default {
required: true,
type: String,
},
- namespace: {
- required: false,
- type: String,
- default: '',
- },
fluxResourcePath: {
required: false,
type: String,
@@ -54,14 +49,12 @@ export default {
variables() {
return {
configuration: this.configuration,
- namespace: this.namespace,
- environmentName: this.environmentName.toLowerCase(),
fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
- !this.namespace || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
+ !this.fluxResourcePath || this.fluxResourcePath?.includes(HELM_RELEASES_RESOURCE_TYPE),
);
},
error(err) {
@@ -73,17 +66,12 @@ export default {
variables() {
return {
configuration: this.configuration,
- namespace: this.namespace,
- environmentName: this.environmentName.toLowerCase(),
fluxResourcePath: this.fluxResourcePath,
};
},
skip() {
return Boolean(
- !this.namespace ||
- this.$apollo.queries.fluxKustomizationStatus.loading ||
- this.hasKustomizations ||
- this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
+ !this.fluxResourcePath || this.fluxResourcePath?.includes(KUSTOMIZATIONS_RESOURCE_TYPE),
);
},
error(err) {
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 7214454c45c..e97720312b0 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -42,12 +42,12 @@ export const CANARY_STATUS = {
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
export const ENVIRONMENTS_SCOPE = {
- AVAILABLE: 'available',
+ ACTIVE: 'active',
STOPPED: 'stopped',
};
export const ENVIRONMENT_COUNT_BY_SCOPE = {
- [ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount',
+ [ENVIRONMENTS_SCOPE.ACTIVE]: 'activeCount',
[ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount',
};
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index 6e3ec04ba3b..bc535eb73aa 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { logError } from '~/lib/logger';
import { toggleQueryPollingByVisibility, etagQueryHeaders } from '~/graphql_shared/utils';
import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 8faed710402..8f57069d89d 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -17,7 +17,6 @@ import typeDefs from './typedefs.graphql';
export const apolloProvider = (endpoint) => {
const defaultClient = createDefaultClient(resolvers(endpoint), {
typeDefs,
- useGet: true,
});
const { cache } = defaultClient;
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
index 7a50ded7d6c..ef5a8194dca 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -1,6 +1,6 @@
query getEnvironmentApp($page: Int, $scope: String, $search: String) {
environmentApp(page: $page, scope: $scope, search: $search) @client {
- availableCount
+ activeCount
stoppedCount
environments
reviewApp
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
index 544232dafd7..042bdc1992d 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
@@ -1,15 +1,6 @@
-query getFluxHelmReleaseStatusQuery(
- $configuration: LocalConfiguration
- $namespace: String
- $environmentName: String
- $fluxResourcePath: String
-) {
- fluxHelmReleaseStatus(
- configuration: $configuration
- namespace: $namespace
- environmentName: $environmentName
- fluxResourcePath: $fluxResourcePath
- ) @client {
+query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxResourcePath: String) {
+ fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
+ @client {
message
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
index 2884f95355e..458b8a4d9db 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
@@ -1,15 +1,9 @@
query getFluxHelmKustomizationStatusQuery(
$configuration: LocalConfiguration
- $namespace: String
- $environmentName: String
$fluxResourcePath: String
) {
- fluxKustomizationStatus(
- configuration: $configuration
- namespace: $namespace
- environmentName: $environmentName
- fluxResourcePath: $fluxResourcePath
- ) @client {
+ fluxKustomizationStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
+ @client {
message
status
type
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index c662acb8f93..ac6a68e450c 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,6 +1,6 @@
query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
folder(environment: $environment, scope: $scope, search: $search) @client {
- availableCount
+ activeCount
environments
stoppedCount
}
diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js
index 9752a3a6634..4427b8ff2ef 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/base.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/base.js
@@ -47,7 +47,7 @@ export const baseQueries = (endpoint) => ({
});
return {
- availableCount: res.data.available_count,
+ activeCount: res.data.active_count,
environments: res.data.environments.map(mapNestedEnvironment),
reviewApp: {
...convertObjectPropsToCamelCase(res.data.review_app),
@@ -61,7 +61,7 @@ export const baseQueries = (endpoint) => ({
},
folder(_, { environment: { folderPath }, scope, search }) {
return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
- availableCount: res.data.available_count,
+ activeCount: res.data.active_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
__typename: 'LocalEnvironmentFolder',
diff --git a/app/assets/javascripts/environments/graphql/resolvers/flux.js b/app/assets/javascripts/environments/graphql/resolvers/flux.js
index d39b1bed7b6..5cb5db5d752 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/flux.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/flux.js
@@ -1,35 +1,83 @@
+import { Configuration, WatchApi, EVENT_DATA } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import {
HELM_RELEASES_RESOURCE_TYPE,
KUSTOMIZATIONS_RESOURCE_TYPE,
} from '~/environments/constants';
+import fluxKustomizationStatusQuery from '../queries/flux_kustomization_status.query.graphql';
+import fluxHelmReleaseStatusQuery from '../queries/flux_helm_release_status.query.graphql';
const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
+const helmReleaseField = 'fluxHelmReleaseStatus';
+const kustomizationField = 'fluxKustomizationStatus';
+
const handleClusterError = (err) => {
const error = err?.response?.data?.message ? new Error(err.response.data.message) : err;
throw error;
};
-const buildFluxResourceUrl = ({
- basePath,
- namespace,
- apiVersion,
- resourceType,
- environmentName = '',
-}) => {
- return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`;
+const buildFluxResourceUrl = ({ basePath, namespace, apiVersion, resourceType }) => {
+ return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`;
};
-const getFluxResourceStatus = (configuration, url) => {
- const { headers } = configuration;
+const buildFluxResourceWatchPath = ({ namespace, apiVersion, resourceType }) => {
+ return `/apis/${apiVersion}/namespaces/${namespace}/${resourceType}`;
+};
+
+const watchFluxResource = ({ watchPath, resourceName, query, variables, field, client }) => {
+ const config = new Configuration(variables.configuration);
+ const watcherApi = new WatchApi(config);
+ const fieldSelector = `metadata.name=${decodeURIComponent(resourceName)}`;
+
+ watcherApi
+ .subscribeToStream(watchPath, { watch: true, fieldSelector })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data[0]?.status?.conditions;
+
+ client.writeQuery({
+ query,
+ variables,
+ data: { [field]: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
+const getFluxResourceStatus = ({ query, variables, field, resourceType, client }) => {
+ const { headers } = variables.configuration;
const withCredentials = true;
+ const url = `${variables.configuration.basePath}/apis/${variables.fluxResourcePath}`;
return axios
.get(url, { withCredentials, headers })
.then((res) => {
- return res?.data?.status?.conditions || [];
+ const fluxData = res?.data;
+ const resourceName = fluxData?.metadata?.name;
+ const namespace = fluxData?.metadata?.namespace;
+ const apiVersion = fluxData?.apiVersion;
+
+ if (gon.features?.k8sWatchApi && resourceName) {
+ const watchPath = buildFluxResourceWatchPath({ namespace, apiVersion, resourceType });
+
+ watchFluxResource({
+ watchPath,
+ resourceName,
+ query,
+ variables,
+ field,
+ client,
+ });
+ }
+
+ return fluxData?.status?.conditions || [];
})
.catch((err) => {
handleClusterError(err);
@@ -62,37 +110,23 @@ const getFluxResources = (configuration, url) => {
};
export default {
- fluxKustomizationStatus(_, { configuration, namespace, environmentName, fluxResourcePath = '' }) {
- let url;
-
- if (fluxResourcePath) {
- url = `${configuration.basePath}/apis/${fluxResourcePath}`;
- } else {
- url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
- apiVersion: kustomizationsApiVersion,
- namespace,
- environmentName,
- });
- }
- return getFluxResourceStatus(configuration, url);
+ fluxKustomizationStatus(_, { configuration, fluxResourcePath }, { client }) {
+ return getFluxResourceStatus({
+ query: fluxKustomizationStatusQuery,
+ variables: { configuration, fluxResourcePath },
+ field: kustomizationField,
+ resourceType: KUSTOMIZATIONS_RESOURCE_TYPE,
+ client,
+ });
},
- fluxHelmReleaseStatus(_, { configuration, namespace, environmentName, fluxResourcePath }) {
- let url;
-
- if (fluxResourcePath) {
- url = `${configuration.basePath}/apis/${fluxResourcePath}`;
- } else {
- url = buildFluxResourceUrl({
- basePath: configuration.basePath,
- resourceType: HELM_RELEASES_RESOURCE_TYPE,
- apiVersion: helmReleasesApiVersion,
- namespace,
- environmentName,
- });
- }
- return getFluxResourceStatus(configuration, url);
+ fluxHelmReleaseStatus(_, { configuration, fluxResourcePath }, { client }) {
+ return getFluxResourceStatus({
+ query: fluxHelmReleaseStatusQuery,
+ variables: { configuration, fluxResourcePath },
+ field: helmReleaseField,
+ resourceType: HELM_RELEASES_RESOURCE_TYPE,
+ client,
+ });
},
fluxKustomizations(_, { configuration, namespace }) {
const url = buildFluxResourceUrl({
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 67a472dac93..8375b8793d9 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -1,5 +1,13 @@
-import { CoreV1Api, Configuration, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
+import {
+ CoreV1Api,
+ Configuration,
+ AppsV1Api,
+ BatchV1Api,
+ WatchApi,
+ EVENT_DATA,
+} from '@gitlab/cluster-client';
import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
+import k8sPodsQuery from '../queries/k8s_pods.query.graphql';
const mapWorkloadItems = (items, kind) => {
return items.map((item) => {
@@ -53,15 +61,50 @@ const handleClusterError = async (err) => {
throw errorData;
};
+const watchPods = ({ configuration, namespace, client }) => {
+ const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods';
+ const config = new Configuration(configuration);
+ const watcherApi = new WatchApi(config);
+
+ watcherApi
+ .subscribeToStream(path, { watch: true })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data.map((item) => {
+ return { status: { phase: item.status.phase } };
+ });
+
+ client.writeQuery({
+ query: k8sPodsQuery,
+ variables: { configuration, namespace },
+ data: { k8sPods: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
export default {
- k8sPods(_, { configuration, namespace }) {
- const coreV1Api = new CoreV1Api(new Configuration(configuration));
+ k8sPods(_, { configuration, namespace }, { client }) {
+ const config = new Configuration(configuration);
+
+ const coreV1Api = new CoreV1Api(config);
const podsApi = namespace
? coreV1Api.listCoreV1NamespacedPod({ namespace })
: coreV1Api.listCoreV1PodForAllNamespaces();
return podsApi
- .then((res) => res?.items || [])
+ .then((res) => {
+ if (gon.features?.k8sWatchApi) {
+ watchPods({ configuration, namespace, client });
+ }
+
+ return res?.items || [];
+ })
.catch(async (err) => {
try {
await handleClusterError(err);
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 1821aa073bc..01879a092ed 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -196,7 +196,7 @@ export default {
text: __('Create issue'),
action: this.createIssue,
extraAttrs: {
- 'data-qa-selector': 'create_issue_button',
+ 'data-testid': 'create-issue-button',
},
};
},
@@ -309,7 +309,7 @@ export default {
<div
v-if="!loadingStacktrace && stacktrace"
class="gl-my-auto gl-text-truncate"
- data-qa-selector="reported_text"
+ data-testid="reported-text"
>
<gl-sprintf :message="__('Reported %{timeAgo} by %{reportedBy}')">
<template #reportedBy>
@@ -367,7 +367,7 @@ export default {
category="primary"
variant="confirm"
:loading="issueCreationInProgress"
- data-qa-selector="create_issue_button"
+ data-testid="create-issue-button"
@click="createIssue"
>
{{ __('Create issue') }}
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index daaeb5f8e85..e0a5e92564e 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -206,7 +206,6 @@ export default {
v-gl-modal="'configure-feature-flags'"
variant="confirm"
category="secondary"
- data-qa-selector="configure_feature_flags_button"
data-testid="ff-configure-button"
class="gl-mb-0 gl-mr-3"
>
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 4d3647cdf5c..74d91734630 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -5,9 +5,10 @@ import {
TOKEN_TYPE_APPROVED_BY,
TOKEN_TYPE_REVIEWER,
TOKEN_TYPE_TARGET_BRANCH,
+ TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
-export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
+export default (IssuableTokenKeys, disableBranchFilter = false) => {
const reviewerToken = {
formattedKey: TOKEN_TITLE_REVIEWER,
key: TOKEN_TYPE_REVIEWER,
@@ -57,7 +58,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
IssuableTokenKeys.tokenKeysWithAlternative.push(draftToken.token);
IssuableTokenKeys.conditions.push(...draftToken.conditions);
- if (!disableTargetBranchFilter) {
+ if (!disableBranchFilter) {
const targetBranchToken = {
formattedKey: __('Target-Branch'),
key: TOKEN_TYPE_TARGET_BRANCH,
@@ -68,8 +69,18 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: 'branch',
};
- IssuableTokenKeys.tokenKeys.push(targetBranchToken);
- IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
+ const sourceBranchToken = {
+ formattedKey: __('Source-Branch'),
+ key: TOKEN_TYPE_SOURCE_BRANCH,
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'branch',
+ tag: 'branch',
+ };
+
+ IssuableTokenKeys.tokenKeys.push(targetBranchToken, sourceBranchToken);
+ IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken, sourceBranchToken);
}
const approvedToken = {
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 892e9130fe8..a1782c549d6 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -11,6 +11,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_REVIEWER,
TOKEN_TYPE_TARGET_BRANCH,
+ TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
@@ -157,6 +158,15 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-target-branch'),
},
+ [TOKEN_TYPE_SOURCE_BRANCH]: {
+ reference: null,
+ gl: DropdownNonUser,
+ extraArguments: {
+ endpoint: this.getMergeRequestSourceBranchesEndpoint(),
+ symbol: '',
+ },
+ element: this.container.querySelector('#js-dropdown-source-branch'),
+ },
environment: {
reference: null,
gl: DropdownNonUser,
@@ -197,10 +207,17 @@ export default class AvailableDropdownMappings {
}
getMergeRequestTargetBranchesEndpoint() {
- const endpoint = `${
- gon.relative_url_root || ''
- }/-/autocomplete/merge_request_target_branches.json`;
+ const targetBranchEndpointPath = '/-/autocomplete/merge_request_target_branches.json';
+ return this.getMergeRequestBranchesEndpoint(targetBranchEndpointPath);
+ }
+
+ getMergeRequestSourceBranchesEndpoint() {
+ const sourceBranchEndpointPath = '/-/autocomplete/merge_request_source_branches.json';
+ return this.getMergeRequestBranchesEndpoint(sourceBranchEndpointPath);
+ }
+ getMergeRequestBranchesEndpoint(endpointPath = '') {
+ const endpoint = `${gon.relative_url_root || ''}${endpointPath}`;
const params = {
group_id: this.getGroupId(),
project_id: this.getProjectId(),
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 99d22b1330b..39a8b1d0a9c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -979,7 +979,7 @@ GfmAutoComplete.Emoji = {
},
filter(query) {
if (query.length === 0) {
- return Object.values(Emoji.getAllEmoji())
+ return Emoji.getAllEmoji()
.map((emoji) => ({
emoji,
fieldValue: emoji.name,
diff --git a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
index f19e047061f..dcf6c90f7fa 100644
--- a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
+++ b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import { captureException } from '@sentry/browser';
+import { captureException } from '~/sentry/sentry_browser_wrapper';
import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml?raw';
import { logError } from '~/lib/logger';
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 5ba46697496..2863f52bea9 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -27,5 +27,6 @@ export const TYPENAME_USER = 'User';
export const TYPENAME_VULNERABILITIES_SCANNER = 'Vulnerabilities::Scanner';
export const TYPENAME_VULNERABILITY = 'Vulnerability';
export const TYPENAME_WORK_ITEM = 'WorkItem';
+export const TYPENAME_ORGANIZATION = 'Organization';
export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply';
export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace';
diff --git a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
index 85a28fe1f71..458fdb24e6d 100644
--- a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
@@ -6,6 +6,7 @@ fragment IssueNode on Issue {
iid
title
referencePath: reference(full: true)
+ closedAt
dueDate
timeEstimate
totalTimeSpent
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 4e0b1413f71..1439a3181b0 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -67,9 +67,11 @@
],
"MemberInterface": [
"GroupMember",
+ "PendingGroupMember",
"ProjectMember"
],
"NoteableInterface": [
+ "AbuseReport",
"AlertManagementAlert",
"BoardEpic",
"Design",
diff --git a/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql b/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql
new file mode 100644
index 00000000000..74da46e5a60
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/groups_autocomplete.query.graphql
@@ -0,0 +1,10 @@
+query groupsAutocomplete($search: String) {
+ groups(search: $search) {
+ nodes {
+ id
+ name
+ fullName
+ avatarUrl
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index fc7cfffc22c..43689e6677b 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -57,7 +57,6 @@ export default {
icon="ellipsis_v"
no-caret
:data-testid="`group-${group.id}-dropdown-button`"
- data-qa-selector="group_dropdown_button"
:data-qa-group-id="group.id"
>
<gl-dropdown-item
diff --git a/app/assets/javascripts/groups/settings/init_access_dropdown.js b/app/assets/javascripts/groups/settings/init_access_dropdown.js
index 4da38e0e641..f18f260097b 100644
--- a/app/assets/javascripts/groups/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/groups/settings/init_access_dropdown.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el) => {
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
index 997e2bc3138..5774065bff9 100644
--- a/app/assets/javascripts/groups_projects/components/transfer_locations.vue
+++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
@@ -260,11 +260,7 @@ export default {
>
<gl-dropdown-divider />
</template>
- <div
- v-if="hasUserTransferLocations"
- data-qa-selector="namespaces_list_users"
- data-testid="user-transfer-locations"
- >
+ <div v-if="hasUserTransferLocations" data-testid="user-transfer-locations">
<gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="item in userTransferLocations"
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 25a84d17379..095a2dc1324 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,3 +1,6 @@
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
import Vue from 'vue';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import { highCountTrim } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
index 2bbad5f3f98..7b26dd183ad 100644
--- a/app/assets/javascripts/header_search/index.js
+++ b/app/assets/javascripts/header_search/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Translate from '~/vue_shared/translate';
import HeaderSearchApp from './components/app.vue';
import createStore from './store';
diff --git a/app/assets/javascripts/header_search/init.js b/app/assets/javascripts/header_search/init.js
index 64502d13ee2..1c582ace480 100644
--- a/app/assets/javascripts/header_search/init.js
+++ b/app/assets/javascripts/header_search/init.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { HEADER_INIT_EVENTS } from './constants';
async function eventHandler(callback = () => {}) {
diff --git a/app/assets/javascripts/helpers/help_page_helper.js b/app/assets/javascripts/helpers/help_page_helper.js
index 21d27b5fea9..fab0f17cd3b 100644
--- a/app/assets/javascripts/helpers/help_page_helper.js
+++ b/app/assets/javascripts/helpers/help_page_helper.js
@@ -1,6 +1,6 @@
import { joinPaths, setUrlFragment } from '~/lib/utils/url_utility';
-const HELP_PAGE_URL_ROOT = '/help/';
+const HELP_PAGE_URL_ROOT = '/help';
/**
* Generate link to a GitLab documentation page.
diff --git a/app/assets/javascripts/helpers/init_simple_app_helper.js b/app/assets/javascripts/helpers/init_simple_app_helper.js
index 695fc455f13..f7bef8c563e 100644
--- a/app/assets/javascripts/helpers/init_simple_app_helper.js
+++ b/app/assets/javascripts/helpers/init_simple_app_helper.js
@@ -1,4 +1,26 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+
+/**
+ * @param {boolean|VueApollo} apolloProviderOption
+ * @returns {undefined | VueApollo}
+ */
+const getApolloProvider = (apolloProviderOption) => {
+ if (apolloProviderOption === true) {
+ Vue.use(VueApollo);
+
+ return new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+ }
+
+ if (apolloProviderOption instanceof VueApollo) {
+ return apolloProviderOption;
+ }
+
+ return undefined;
+};
/**
* Initializes a component as a simple vue app, passing the necessary props. If the element
@@ -8,6 +30,8 @@ import Vue from 'vue';
*
* @param {string} selector css selector for where to build
* @param {Vue.component} component The Vue compoment to be built as the root of the app
+ * @param {{withApolloProvider: boolean|VueApollo}} options. extra options to be passed to the vue app
+ * withApolloProvider: if true, instantiates a default apolloProvider. Also accepts and instance of VueApollo
*
* @example
* ```html
@@ -15,13 +39,13 @@ import Vue from 'vue';
* ```
*
* ```javascript
- * initSimpleApp('#mount-here', MyApp)
+ * initSimpleApp('#mount-here', MyApp, { withApolloProvider: true })
* ```
*
* This will mount MyApp as root on '#mount-here'. It will receive {'some': 'object'} as it's
* view model prop.
*/
-export const initSimpleApp = (selector, component) => {
+export const initSimpleApp = (selector, component, { withApolloProvider } = {}) => {
const element = document.querySelector(selector);
if (!element) {
@@ -32,6 +56,7 @@ export const initSimpleApp = (selector, component) => {
return new Vue({
el: element,
+ apolloProvider: getApolloProvider(withApolloProvider),
render(h) {
return h(component, { props });
},
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 76b284b6185..984dc9edaf1 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -88,7 +88,6 @@ export default {
@click="openRightPane($options.rightSidebarViews.pipelines)"
>
<ci-icon
- v-gl-tooltip
:status="latestPipeline.details.status"
:title="latestPipeline.details.status.text"
/>
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index f0c5b29e210..f5840661c17 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -25,9 +25,7 @@ export default {
<template>
<div class="d-flex align-items-center">
<ci-icon
- is-borderless
:status="job.status"
- :size="24"
class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
/>
<span class="gl-ml-3">
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 6bf51ed06a6..0e07cc34dd8 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -63,7 +63,7 @@ export default {
</div>
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
- <ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
+ <ci-icon :status="latestPipeline.details.status" />
<span class="gl-ml-3">
<strong> {{ __('Pipeline') }} </strong>
<a
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 137df9aa102..3b59fe86764 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -8,7 +8,6 @@ import {
EDITOR_TYPE_CODE,
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
- EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
@@ -30,6 +29,7 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { markRaw } from '~/lib/utils/vue3compat/mark_raw';
import { readFileAsDataURL } from '~/lib/utils/file_utility';
+import { isDefaultCiConfig, hasCiConfigExtension } from '~/lib/utils/common_utils';
import {
leftSidebarViews,
@@ -152,8 +152,9 @@ export default {
},
isCiConfigFile() {
return (
- this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH &&
- this.editor?.getEditorType() === EDITOR_TYPE_CODE
+ // For CI config schemas the filename must match '*.gitlab-ci.yml' regardless of project configuration.
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/293641
+ hasCiConfigExtension(this.file.path) && this.editor?.getEditorType() === EDITOR_TYPE_CODE
);
},
},
@@ -162,7 +163,7 @@ export default {
handler() {
this.stopWatchingCiYaml();
- if (this.file.name === '.gitlab-ci.yml') {
+ if (isDefaultCiConfig(this.file.name)) {
this.startWatchingCiYaml();
}
},
diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js
index c9db9779b1f..ac4eeb0386f 100644
--- a/app/assets/javascripts/ide/lib/alerts/index.js
+++ b/app/assets/javascripts/ide/lib/alerts/index.js
@@ -1,3 +1,4 @@
+import { isDefaultCiConfig } from '~/lib/utils/common_utils';
import { leftSidebarViews } from '../../constants';
import EnvironmentsMessage from './environments.vue';
@@ -6,7 +7,7 @@ const alerts = [
key: Symbol('ALERT_ENVIRONMENT'),
show: (state, file) =>
state.currentActivityView === leftSidebarViews.commit.name &&
- file.path === '.gitlab-ci.yml' &&
+ isDefaultCiConfig(file.path) &&
state.environmentsGuidanceAlertDetected &&
!state.environmentsGuidanceAlertDismissed,
props: { variant: 'tip' },
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index bf0d3ed337c..5681f6cdec5 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,9 +1,10 @@
import { __ } from '~/locale';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import { leftSidebarViews } from '../../../constants';
export const templateTypes = () => [
{
- name: '.gitlab-ci.yml',
+ name: DEFAULT_CI_CONFIG_PATH,
key: 'gitlab_ci_ymls',
},
{
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
index 37f40af9c2e..8adde8f6b4e 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js
@@ -48,7 +48,7 @@ export default {
},
[types.SET_SESSION_STATUS](state, status) {
const session = {
- ...(state.session || {}),
+ ...state.session,
status,
};
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index ddf69a8fcdf..b02eb3c4307 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -1,6 +1,18 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
+export const BULK_IMPORT_STATIC_ITEMS = {
+ badges: __('Badge'),
+ boards: s__('IssueBoards|Board'),
+ epics: __('Epic'),
+ issues: __('Issue'),
+ labels: __('Label'),
+ members: __('Member'),
+ merge_requests: __('Merge request'),
+ milestones: __('Milestone'),
+ project: __('Project'),
+};
+
const STATISTIC_ITEMS = {
diff_note: __('Diff notes'),
issue: __('Issues'),
diff --git a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
new file mode 100644
index 00000000000..5da16454032
--- /dev/null
+++ b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
@@ -0,0 +1,44 @@
+<script>
+import { __ } from '~/locale';
+import ImportDetailsTable from '~/import/details/components/import_details_table.vue';
+
+export default {
+ name: 'BulkImportDetailsApp',
+ components: { ImportDetailsTable },
+
+ fields: [
+ {
+ key: 'relation',
+ label: __('Type'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'source_title',
+ label: __('Title'),
+ tdClass: 'gl-md-w-30 gl-word-break-word',
+ },
+ {
+ key: 'error',
+ label: __('Error'),
+ },
+ {
+ key: 'correlation_id_value',
+ label: __('Correlation ID'),
+ },
+ ],
+
+ LOCAL_STORAGE_KEY: 'gl-bulk-import-details-page-size',
+};
+</script>
+
+<template>
+ <div>
+ <h1>{{ s__('Import|GitLab Migration details') }}</h1>
+
+ <import-details-table
+ bulk-import
+ :fields="$options.fields"
+ :local-storage-key="$options.LOCAL_STORAGE_KEY"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/import/details/components/import_details_app.vue b/app/assets/javascripts/import/details/components/import_details_app.vue
index 13483fa8ba2..f654dc61e07 100644
--- a/app/assets/javascripts/import/details/components/import_details_app.vue
+++ b/app/assets/javascripts/import/details/components/import_details_app.vue
@@ -1,18 +1,44 @@
<script>
-import { s__ } from '~/locale';
+import { __ } from '~/locale';
import ImportDetailsTable from './import_details_table.vue';
export default {
+ name: 'ImportDetailsApp',
components: { ImportDetailsTable },
- i18n: {
- pageTitle: s__('Import|GitHub import details'),
- },
+
+ fields: [
+ {
+ key: 'type',
+ label: __('Type'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'title',
+ label: __('Title'),
+ tdClass: 'gl-md-w-30 gl-word-break-word',
+ },
+ {
+ key: 'provider_url',
+ label: __('URL'),
+ tdClass: 'gl-white-space-nowrap',
+ },
+ {
+ key: 'details',
+ label: __('Details'),
+ },
+ ],
+
+ LOCAL_STORAGE_KEY: 'gl-import-details-page-size',
};
</script>
<template>
<div>
- <h1>{{ $options.i18n.pageTitle }}</h1>
- <import-details-table />
+ <h1>{{ s__('Import|GitHub import details') }}</h1>
+
+ <import-details-table
+ :fields="$options.fields"
+ :local-storage-key="$options.LOCAL_STORAGE_KEY"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/import/details/components/import_details_table.vue b/app/assets/javascripts/import/details/components/import_details_table.vue
index 813dc1f2645..535ccb525ac 100644
--- a/app/assets/javascripts/import/details/components/import_details_table.vue
+++ b/app/assets/javascripts/import/details/components/import_details_table.vue
@@ -1,12 +1,13 @@
<script>
import { GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
-import { STATISTIC_ITEMS } from '../../constants';
+import { getBulkImportFailures } from '~/rest_api';
+import { BULK_IMPORT_STATIC_ITEMS, STATISTIC_ITEMS } from '../../constants';
import { fetchImportFailures } from '../api';
const DEFAULT_PAGE_SIZE = 20;
@@ -21,28 +22,6 @@ export default {
PaginationBar,
},
STATISTIC_ITEMS,
- LOCAL_STORAGE_KEY: 'gl-import-details-page-size',
- fields: [
- {
- key: 'type',
- label: __('Type'),
- tdClass: 'gl-white-space-nowrap',
- },
- {
- key: 'title',
- label: __('Title'),
- tdClass: 'gl-md-w-30 gl-word-break-word',
- },
- {
- key: 'provider_url',
- label: __('URL'),
- tdClass: 'gl-white-space-nowrap',
- },
- {
- key: 'details',
- label: __('Details'),
- },
- ],
i18n: {
fetchErrorMessage: s__('Import|An error occurred while fetching import details.'),
@@ -55,6 +34,25 @@ export default {
},
},
+ props: {
+ bulkImport: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+
+ fields: {
+ type: Array,
+ required: true,
+ },
+
+ localStorageKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
data() {
return {
items: [],
@@ -97,18 +95,28 @@ export default {
this.loadImportFailures();
},
+ fetchFn(params) {
+ return this.bulkImport
+ ? getBulkImportFailures(
+ getParameterValues('id')[0],
+ getParameterValues('entity_id')[0],
+ params,
+ )
+ : fetchImportFailures(this.failuresPath, {
+ projectId: getParameterValues('project_id')[0],
+ ...params,
+ });
+ },
+
async loadImportFailures() {
- if (!this.failuresPath) {
+ if (!this.bulkImport && !this.failuresPath) {
return;
}
this.loading = true;
+
try {
- const response = await fetchImportFailures(this.failuresPath, {
- projectId: getParameterValues('project_id')[0],
- page: this.page,
- perPage: this.perPage,
- });
+ const response = await this.fetchFn({ page: this.page, perPage: this.perPage });
const { page, perPage, totalPages, total } = parseIntPagination(
normalizeHeaders(response.headers),
@@ -123,13 +131,17 @@ export default {
}
this.loading = false;
},
+
+ itemTypeText(type) {
+ return (this.bulkImport ? BULK_IMPORT_STATIC_ITEMS[type] : STATISTIC_ITEMS[type]) || type;
+ },
},
};
</script>
<template>
<div>
- <gl-table :fields="$options.fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
+ <gl-table :fields="fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
<template #table-busy>
<gl-loading-icon size="lg" class="gl-my-5" />
</template>
@@ -139,7 +151,7 @@ export default {
</template>
<template #cell(type)="{ item: { type } }">
- {{ $options.STATISTIC_ITEMS[type] }}
+ {{ itemTypeText(type) }}
</template>
<template #cell(provider_url)="{ item: { provider_url } }">
<gl-link v-if="provider_url" :href="provider_url" target="_blank">
@@ -147,12 +159,30 @@ export default {
<gl-icon name="external-link" />
</gl-link>
</template>
+
+ <template #cell(relation)="{ item: { relation } }">
+ {{ itemTypeText(relation) }}
+ </template>
+ <template #cell(source_title)="{ item: { source_title, source_url } }">
+ <gl-link v-if="source_url" :href="source_url" target="_blank">
+ {{ source_title }}
+ <gl-icon name="external-link" />
+ </gl-link>
+ <span v-else>
+ {{ source_title }}
+ </span>
+ </template>
+ <template #cell(error)="{ item: { exception_class, exception_message } }">
+ <strong>{{ exception_class }}</strong>
+ <p>{{ exception_message }}</p>
+ </template>
</gl-table>
+
<pagination-bar
v-if="hasItems"
:page-info="pageInfo"
class="gl-mt-5"
- :storage-key="$options.LOCAL_STORAGE_KEY"
+ :storage-key="localStorageKey"
@set-page="setPage"
@set-page-size="setPageSize"
/>
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 91436457b03..3cde3a8df3c 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -1,46 +1,9 @@
<script>
import { GlAccordion, GlAccordionItem, GlBadge, GlIcon, GlLink } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
+import { s__ } from '~/locale';
import { STATISTIC_ITEMS } from '~/import/constants';
-import { STATUSES } from '../constants';
-
-const SCHEDULED_STATUS = {
- icon: 'status-scheduled',
- text: __('Pending'),
- variant: 'muted',
-};
-
-const STATUS_MAP = {
- [STATUSES.NONE]: {
- icon: 'status-waiting',
- text: __('Not started'),
- variant: 'muted',
- },
- [STATUSES.SCHEDULING]: SCHEDULED_STATUS,
- [STATUSES.SCHEDULED]: SCHEDULED_STATUS,
- [STATUSES.CREATED]: SCHEDULED_STATUS,
- [STATUSES.STARTED]: {
- icon: 'status-running',
- text: __('Importing...'),
- variant: 'info',
- },
- [STATUSES.FAILED]: {
- icon: 'status-failed',
- text: __('Failed'),
- variant: 'danger',
- },
- [STATUSES.TIMEOUT]: {
- icon: 'status-failed',
- text: __('Timeout'),
- variant: 'danger',
- },
- [STATUSES.CANCELED]: {
- icon: 'status-stopped',
- text: __('Cancelled'),
- variant: 'neutral',
- },
-};
+import { STATUSES, STATUS_ICON_MAP } from '../constants';
function isIncompleteImport(stats) {
return Object.keys(stats?.fetched ?? []).some(
@@ -96,21 +59,11 @@ export default {
},
mappedStatus() {
- if (this.status === STATUSES.FINISHED) {
- return this.isIncomplete
- ? {
- icon: 'status-alert',
- text: s__('Import|Partially completed'),
- variant: 'warning',
- }
- : {
- icon: 'status-success',
- text: __('Complete'),
- variant: 'success',
- };
+ if (this.isIncomplete) {
+ return STATUS_ICON_MAP[STATUSES.PARTIAL];
}
- return STATUS_MAP[this.status];
+ return STATUS_ICON_MAP[this.status];
},
showDetails() {
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index 48b7febca4b..23604c7fb44 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -1,18 +1,65 @@
-// The `scheduling` status is only present on the client-side,
-// it is used as the status when we are requesting to start an import.
+import { __, s__ } from '~/locale';
export const STATUSES = {
FINISHED: 'finished',
FAILED: 'failed',
SCHEDULED: 'scheduled',
+ SCHEDULING: 'scheduling', // only present client-side, used when user is requesting to start an import
CREATED: 'created',
STARTED: 'started',
NONE: 'none',
- SCHEDULING: 'scheduling',
CANCELED: 'canceled',
TIMEOUT: 'timeout',
+ PARTIAL: 'partial', // only present client-side, finished but with failures
};
export const PROVIDERS = {
GITHUB: 'github',
};
+
+const SCHEDULED_STATUS_ICON = {
+ icon: 'status-scheduled',
+ text: __('Pending'),
+ variant: 'muted',
+};
+
+export const STATUS_ICON_MAP = {
+ [STATUSES.NONE]: {
+ icon: 'status-waiting',
+ text: __('Not started'),
+ variant: 'muted',
+ },
+ [STATUSES.SCHEDULING]: SCHEDULED_STATUS_ICON,
+ [STATUSES.SCHEDULED]: SCHEDULED_STATUS_ICON,
+ [STATUSES.CREATED]: SCHEDULED_STATUS_ICON,
+ [STATUSES.STARTED]: {
+ icon: 'status-running',
+ text: __('Importing...'),
+ variant: 'info',
+ },
+ [STATUSES.FAILED]: {
+ icon: 'status-failed',
+ text: __('Failed'),
+ variant: 'danger',
+ },
+ [STATUSES.TIMEOUT]: {
+ icon: 'status-failed',
+ text: __('Timeout'),
+ variant: 'danger',
+ },
+ [STATUSES.CANCELED]: {
+ icon: 'status-stopped',
+ text: __('Cancelled'),
+ variant: 'neutral',
+ },
+ [STATUSES.FINISHED]: {
+ icon: 'status-success',
+ text: __('Complete'),
+ variant: 'success',
+ },
+ [STATUSES.PARTIAL]: {
+ icon: 'status-alert',
+ text: s__('Import|Partially completed'),
+ variant: 'warning',
+ },
+};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_status.vue b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
new file mode 100644
index 00000000000..cdb38cdf7f1
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlBadge, GlLink } from '@gitlab/ui';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { STATUSES, STATUS_ICON_MAP } from '~/import_entities/constants';
+
+export default {
+ components: {
+ GlBadge,
+ GlLink,
+ },
+
+ inject: {
+ detailsPath: {
+ default: undefined,
+ },
+ },
+
+ props: {
+ id: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ entityId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ hasFailures: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showDetailsLink: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ isPartial() {
+ return this.status === STATUSES.FINISHED && this.hasFailures;
+ },
+
+ mappedStatus() {
+ if (this.isPartial) {
+ return STATUS_ICON_MAP[STATUSES.PARTIAL];
+ }
+
+ return STATUS_ICON_MAP[this.status];
+ },
+
+ showDetails() {
+ return this.showDetailsLink && Boolean(this.detailsPathWithId) && this.hasFailures;
+ },
+
+ detailsPathWithId() {
+ if (!this.id || !this.entityId || !this.detailsPath) {
+ return null;
+ }
+
+ return mergeUrlParams({ id: this.id, entity_id: this.entityId }, this.detailsPath);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-badge :icon="mappedStatus.icon" :variant="mappedStatus.variant" size="md" icon-size="sm">
+ {{ mappedStatus.text }}
+ </gl-badge>
+
+ <div v-if="showDetails" class="gl-mt-2">
+ <gl-link :href="detailsPathWithId">{{ s__('Import|See failures') }}</gl-link>
+ </div>
+ </div>
+</template>
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 24197c680eb..df1e50cb433 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
@@ -42,9 +42,6 @@ import ImportTargetCell from './import_target_cell.vue';
const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
const PAGE_SIZES = [20, 50, 100];
const DEFAULT_PAGE_SIZE = PAGE_SIZES[0];
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
-const DEFAULT_TD_CLASSES = 'gl-vertical-align-top!';
export default {
components: {
@@ -129,36 +126,28 @@ export default {
{
key: 'selected',
label: '',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- thClass: `${DEFAULT_TH_CLASSES} gl-w-3 gl-pr-3!`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pr-3!`,
+ thClass: 'gl-w-3 gl-pr-3!',
+ tdClass: 'gl-pr-3!',
},
{
key: 'webUrl',
label: s__('BulkImport|Source group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! gl-w-half`,
- // eslint-disable-next-line @gitlab/require-i18n-strings
- tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
+ thClass: 'gl-pl-0! gl-w-half',
+ tdClass: 'gl-pl-0!',
},
{
key: 'importTarget',
label: s__('BulkImport|New group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-half`,
- tdClass: DEFAULT_TD_CLASSES,
+ thClass: `gl-w-half`,
},
{
key: 'progress',
label: __('Status'),
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
tdAttr: { 'data-qa-selector': 'import_status_indicator' },
},
{
key: 'actions',
label: '',
- thClass: `${DEFAULT_TH_CLASSES}`,
- tdClass: DEFAULT_TD_CLASSES,
},
],
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
index 1aad22f0f3f..c2e35ce8270 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -60,7 +60,7 @@ export class LocalStorageCache {
updateStatusByJobId(jobId, status) {
this.getCacheKeysByJobId(jobId).forEach((webUrl) =>
this.set(webUrl, {
- ...(this.get(webUrl) ?? {}),
+ ...this.get(webUrl),
progress: {
id: jobId,
status,
diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
index cf1a4de68ed..d22a52df326 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
@@ -41,7 +41,7 @@ export default {
:key="name"
:checked="value[name]"
:data-qa-option-name="name"
- data-qa-selector="advanced_settings_checkbox"
+ data-testid="advanced-settings-checkbox"
@change="$emit('input', { ...value, [name]: $event })"
>
{{ label }}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
index 5d5965e33da..72c6f45cdc9 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_organizations_box.vue
@@ -1,6 +1,6 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlCollapsibleListbox } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
index cb3476c48db..5931e0d307a 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/github_status_table.vue
@@ -78,7 +78,6 @@ export default {
@input="setFilter({ organization_login: $event })"
/>
<gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
name="filter"
:disabled="isNameFilterDisabled"
:value="nameFilter"
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 009945f8b9b..d98132382c6 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -155,7 +155,6 @@ export default {
<slot name="actions"></slot>
<form v-if="filterable" class="gl-ml-auto" novalidate @submit.prevent>
<gl-search-box-by-click
- data-qa-selector="githubish_import_filter_field"
name="filter"
:placeholder="__('Filter by name')"
autofocus
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index d75ba53d727..9b5aff45375 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -159,7 +159,7 @@ export default {
<template>
<tr
class="gl-h-11"
- data-qa-selector="project_import_row"
+ data-testid="project-import-row"
:data-qa-source-project="repo.importSource.fullName"
>
<td>
@@ -174,7 +174,7 @@ export default {
:href="repo.importedProject.fullPath"
class="gl-font-sm"
target="_blank"
- data-qa-selector="go_to_project_link"
+ data-testid="go-to-project-link"
>
{{ displayFullPath }}
</gl-link>
@@ -182,7 +182,7 @@ export default {
</gl-sprintf>
</div>
</td>
- <td data-testid="fullPath" data-qa-selector="project_path_content">
+ <td data-testid="fullPath">
<div class="gl-display-flex gl-sm-flex-wrap">
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted || isSelectedForReimport">
@@ -201,14 +201,14 @@ export default {
ref="newNameInput"
v-model="newNameInput"
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
- data-qa-selector="project_path_field"
+ data-testid="project-path-field"
/>
</div>
</template>
<template v-else-if="repo.importedProject">{{ displayFullPath }}</template>
</div>
</td>
- <td data-qa-selector="import_status_indicator">
+ <td data-testid="import-status-indicator">
<import-status :project-id="importedProjectId" :status="importStatus" :stats="stats" />
</td>
<td data-testid="actions" class="gl-white-space-nowrap">
@@ -235,7 +235,7 @@ export default {
<gl-button
v-if="isImportNotStarted || isFinished"
type="button"
- data-qa-selector="import_button"
+ data-testid="import-button"
@click="handleImportRepo()"
>
{{ importButtonText }}
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index 4305f8d4db5..e5cbac71ce0 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -83,7 +83,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
.get(
pathWithParams({
path: reposPath,
- ...(filter ?? {}),
+ ...filter,
...paginationParams({ state }),
}),
)
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index 0e1afebbe2b..727ab43435d 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -87,12 +87,11 @@ export default {
{
key: 'incidentSla',
label: s__('IncidentManagement|Time to SLA'),
- thClass: `gl-text-right gl-w-10p`,
+ thClass: `${thClass} gl-text-right gl-w-10p`,
tdClass: `${tdClass} gl-text-right`,
thAttr: TH_INCIDENT_SLA_TEST_ID,
actualSortKey: 'SLA_DUE_AT',
sortable: true,
- sortDirection: 'asc',
},
{
key: 'assignees',
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index fa9a59212eb..281666a021d 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,9 +1,9 @@
<script>
import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
-import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import SafeHtml from '~/vue_shared/directives/safe_html';
import {
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
index a8389e32b40..356557442db 100644
--- a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlLoadingIcon, GlPagination, GlTable, GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { DEFAULT_PER_PAGE } from '~/api';
import { fetchOverrides } from '~/integrations/overrides/api';
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 4b492e48095..ceb9200dfad 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlAlert } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
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 509efd31dcd..1a10130e969 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,13 +1,17 @@
<script>
-import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlCollapse, GlLink, GlIcon, GlSprintf } 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 { n__, s__, sprintf } from '~/locale';
+import {
+ memberName,
+ triggerExternalAlert,
+ inviteMembersTrackingOptions,
+} from 'ee_else_ce/invite_members/utils/member_utils';
import { captureException } from '~/ci/runner/sentry_utils';
import {
USERS_FILTER_ALL,
@@ -31,7 +35,9 @@ export default {
GlAlert,
GlButton,
GlCollapse,
+ GlLink,
GlIcon,
+ GlSprintf,
InviteModalBase,
MembersTokenSelect,
ModalConfetti,
@@ -43,6 +49,17 @@ export default {
SafeHtml,
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
+ inject: {
+ isCurrentUserAdmin: {
+ default: false,
+ },
+ isEmailSignupEnabled: {
+ default: true,
+ },
+ newUsersUrl: {
+ default: '',
+ },
+ },
props: {
id: {
type: String,
@@ -122,6 +139,12 @@ export default {
isCelebration() {
return this.mode === 'celebrate';
},
+ baseTrackingDetails() {
+ return { label: this.source, celebrate: this.isCelebration };
+ },
+ isTextForAdmin() {
+ return this.isCurrentUserAdmin && Boolean(this.newUsersUrl);
+ },
modalTitle() {
return this.$options.labels.modal[this.mode].title;
},
@@ -131,6 +154,11 @@ export default {
labelIntroText() {
return this.$options.labels[this.inviteTo][this.mode].introText;
},
+ labelSearchField() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.searchField
+ : s__('InviteMembersModal|Username');
+ },
isEmptyInvites() {
return Boolean(this.newUsersToInvite.length);
},
@@ -144,6 +172,14 @@ export default {
this.errorList.length,
);
},
+ signupDisabledText() {
+ return s__(
+ "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username.",
+ );
+ },
+ signupDisabledTitle() {
+ return s__('InviteMembersModal|Inviting users by email is disabled');
+ },
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
@@ -173,8 +209,13 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescriptionText() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.placeHolder
+ : s__('InviteMembersModal|Select members');
+ },
formGroupDescription() {
- return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ return this.invalidFeedbackMessage ? null : this.formGroupDescriptionText;
},
},
watch: {
@@ -218,13 +259,13 @@ export default {
this.source = source;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
- this.track('render', { label: this.source });
+ this.track('render', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
showEmptyInvitesAlert() {
- this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.invalidFeedbackMessage = this.formGroupDescriptionText;
this.shouldShowEmptyInvitesAlert = true;
this.$refs.alerts.focus();
},
@@ -287,10 +328,10 @@ export default {
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
},
onCancel() {
- this.track('click_cancel', { label: this.source });
+ this.track('click_cancel', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
onClose() {
- this.track('click_x', { label: this.source });
+ this.track('click_x', inviteMembersTrackingOptions(this.baseTrackingDetails));
},
resetFields() {
this.clearValidation();
@@ -299,7 +340,7 @@ export default {
this.newUsersToInvite = [];
},
onInviteSuccess() {
- this.track('invite_successful', { label: this.source });
+ this.track('invite_successful', inviteMembersTrackingOptions(this.baseTrackingDetails));
if (this.reloadPageOnSubmit) {
reloadOnInvitationSuccess();
@@ -345,7 +386,7 @@ export default {
:default-access-level="defaultAccessLevel"
:help-link="helpLink"
:label-intro-text="labelIntroText"
- :label-search-field="$options.labels.searchField"
+ :label-search-field="labelSearchField"
:form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
@@ -429,6 +470,24 @@ export default {
</gl-button>
</template>
</gl-alert>
+ <gl-alert
+ v-if="!isEmailSignupEnabled"
+ id="signup-disabled-alert"
+ :dismissible="false"
+ :title="signupDisabledTitle"
+ class="gl-mb-4"
+ variant="warning"
+ data-testid="email-signup-disabled-alert"
+ >
+ <gl-sprintf :message="signupDisabledText">
+ <template #link="{ content }">
+ <gl-link v-if="isTextForAdmin" :href="newUsersUrl" target="_blank">{{
+ content
+ }}</gl-link>
+ <span v-else>{{ content }}</span>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<user-limit-notification
v-else-if="showUserLimitNotification"
class="gl-mb-5"
@@ -447,6 +506,7 @@ export default {
v-model="newUsersToInvite"
class="gl-mb-2"
aria-labelledby="empty-invites-alert"
+ :can-use-email-token="isEmailSignupEnabled"
:input-id="inputId"
:exception-state="exceptionState"
:users-filter="usersFilter"
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 18d22395104..a14dcd38aa7 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -297,7 +297,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="gl-w-half gl-xs-w-full"
+ class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_LEVEL"
:label-for="dropdownId"
>
@@ -317,7 +317,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="gl-w-half gl-xs-w-full"
+ class="gl-sm-w-half gl-w-full"
:label="$options.ACCESS_EXPIRE_DATE"
:label-for="datepickerId"
>
@@ -338,10 +338,10 @@ export default {
<template #modal-footer>
<div
- class="gl-m-0 gl-xs-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
+ class="gl-m-0 gl-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
>
<gl-button
- class="gl-xs-w-full gl-xs-mb-3! gl-sm-ml-3!"
+ class="gl-w-full gl-sm-w-auto gl-xs-mb-3! gl-sm-ml-3!"
data-testid="invite-modal-submit"
v-bind="actionPrimary.attributes"
@click="onSubmit"
@@ -350,7 +350,7 @@ export default {
</gl-button>
<gl-button
- class="gl-xs-w-full"
+ class="gl-w-full gl-sm-w-auto"
data-testid="invite-modal-cancel"
v-bind="actionCancel.attributes"
@click="onCancel"
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 8493787f075..0be04b7af35 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -21,6 +21,11 @@ export default {
GlSprintf,
},
props: {
+ canUseEmailToken: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
placeholder: {
type: String,
required: false,
@@ -68,6 +73,10 @@ export default {
},
computed: {
emailIsValid() {
+ if (!this.canUseEmailToken) {
+ return false;
+ }
+
const regex = /^\S+@\S+$/;
return this.originalInput.match(regex) !== null;
@@ -137,9 +146,8 @@ export default {
username: token.username,
avatar_url: token.avatar_url,
}));
- this.loading = false;
})
- .catch(() => {
+ .finally(() => {
this.loading = false;
});
}, SEARCH_DELAY),
diff --git a/app/assets/javascripts/invite_members/components/project_select.vue b/app/assets/javascripts/invite_members/components/project_select.vue
index 640df5cdb88..6c2f53afe3c 100644
--- a/app/assets/javascripts/invite_members/components/project_select.vue
+++ b/app/assets/javascripts/invite_members/components/project_select.vue
@@ -115,7 +115,6 @@ export default {
:search-placeholder="$options.i18n.searchPlaceholder"
:no-results-text="$options.i18n.emptySearchResult"
data-testid="project-select-dropdown"
- data-qa-selector="project_select_dropdown"
class="gl-collapsible-listbox-w-full"
@search="searchTerm = $event"
@select="selectProject"
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 41ed0179364..8dfe697e2cb 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -25,6 +25,9 @@ export default (function initInviteMembersModal() {
name: 'InviteMembersModalRoot',
provide: {
name: el.dataset.name,
+ newUsersUrl: el.dataset.newUsersUrl,
+ isCurrentUserAdmin: parseBoolean(el.dataset.isCurrentUserAdmin),
+ isEmailSignupEnabled: parseBoolean(el.dataset.isSignupEnabled),
},
render: (createElement) =>
createElement(InviteMembersModal, {
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
index 7998cb69445..52fb5e98f27 100644
--- a/app/assets/javascripts/invite_members/utils/member_utils.js
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -6,3 +6,7 @@ export function memberName(member) {
export function triggerExternalAlert() {
return false;
}
+
+export function inviteMembersTrackingOptions(options) {
+ return { label: options.label };
+}
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
index f97ac888417..652d02e8f9d 100644
--- a/app/assets/javascripts/issuable/components/locked_badge.vue
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -20,9 +20,12 @@ export default {
},
computed: {
title() {
- return sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: issuableTypeText[this.issuableType],
- });
+ return sprintf(
+ __('The discussion in this %{issuable} is locked. Only project members can comment.'),
+ {
+ issuable: issuableTypeText[this.issuableType],
+ },
+ );
},
},
};
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 71bd301162e..126a3a84d66 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -88,6 +88,9 @@ export default {
workItemIid() {
return String(this.iid);
},
+ pipelinePath() {
+ return this.pipelineStatus?.details_path || this.pipelineStatus?.detailsPath;
+ },
},
methods: {
handleTitleClick(event) {
@@ -191,16 +194,16 @@ export default {
<div
class="item-attributes-area gl-display-flex gl-align-items-center gl-flex-wrap gl-gap-3"
>
- <span v-if="hasPipeline" class="mr-ci-status order-md-last">
- <a :href="pipelineStatus.details_path">
- <ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
+ <span v-if="hasPipeline" class="mr-ci-status order-md-last gl-md-ml-3 gl-mr-n2">
+ <a :href="pipelinePath">
+ <ci-icon :status="pipelineStatus" :title="pipelineStatusTooltip" />
</a>
</span>
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
- class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first"
+ class="item-milestone gl-font-sm gl-display-flex gl-align-items-center order-md-first gl-ml-2"
/>
<!-- Flex order for slots is defined in the parent component: e.g. related_issues_block.vue -->
diff --git a/app/assets/javascripts/issuable/components/status_badge.vue b/app/assets/javascripts/issuable/components/status_badge.vue
index 949fb3c1ce5..35f6446d582 100644
--- a/app/assets/javascripts/issuable/components/status_badge.vue
+++ b/app/assets/javascripts/issuable/components/status_badge.vue
@@ -14,29 +14,29 @@ import {
const badgePropertiesMap = {
[TYPE_EPIC]: {
[STATUS_OPEN]: {
- icon: 'epic',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'epic-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
},
[TYPE_ISSUE]: {
[STATUS_OPEN]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
[STATUS_CLOSED]: {
- icon: 'issue-closed',
+ icon: 'issue-close',
text: __('Closed'),
variant: 'info',
},
[STATUS_LOCKED]: {
- icon: 'issues',
+ icon: 'issue-open-m',
text: __('Open'),
variant: 'success',
},
diff --git a/app/assets/javascripts/issuable/popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
index e2c2181684f..80ae8ed8cf6 100644
--- a/app/assets/javascripts/issuable/popover/components/mr_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
@@ -96,14 +96,14 @@ export default {
</gl-skeleton-loader>
<div v-else-if="showDetails" class="d-flex align-items-center justify-content-between">
<div class="d-inline-flex align-items-center">
- <gl-badge class="gl-mr-3" :variant="badgeVariant">
+ <gl-badge class="gl-mr-2" :variant="badgeVariant">
{{ stateHumanName }}
</gl-badge>
<span class="gl-text-secondary">
{{ __('Opened') }} <time v-text="formattedTime"></time
></span>
</div>
- <ci-icon v-if="detailedStatus" :status="detailedStatus" />
+ <ci-icon v-if="detailedStatus" :status="detailedStatus" class="gl-ml-2" />
</div>
<h5 v-if="!$apollo.queries.mergeRequest.loading" class="my-2">{{ title }}</h5>
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index a756229e6ca..b6465cf6c68 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -5,7 +5,7 @@ import {
GlFilteredSearchToken,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
@@ -165,9 +165,6 @@ export default {
skip() {
return !this.hasSearch;
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index 06bbcdc12ea..b83db65caa6 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -53,6 +53,8 @@ export default class Issue {
$(document).trigger('issuable:change', isClosed);
+ // TODO: Remove this with the removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
let numProjectIssues = Number(
projectIssuesCounter.first().text().trim().replace(/[^\d]/, ''),
);
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 16e687cff10..72bb88ef1d5 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -9,11 +9,11 @@ import {
GlDrawer,
GlLink,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
@@ -277,9 +277,6 @@ export default {
skip() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
@@ -910,7 +907,7 @@ export default {
v-if="issuesDrawerEnabled"
:open="isIssuableSelected"
header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
- class="gl-w-40p gl-xs-w-full"
+ class="gl-w-full gl-sm-w-40p"
@close="activeIssuable = null"
>
<template #title>
@@ -1030,7 +1027,10 @@ export default {
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
/>
- <gl-disclosure-dropdown-group :bordered="true" :group="subscribeDropdownOptions" />
+ <gl-disclosure-dropdown-group
+ :bordered="showCsvButtons"
+ :group="subscribeDropdownOptions"
+ />
</gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
index 4b59672428b..eb7bcf70563 100644
--- a/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { isEmpty } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
@@ -166,9 +166,6 @@ export default {
skip() {
return this.shouldSkipQuery;
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 10323b99665..1f159e71da9 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -79,7 +79,6 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
supports-quick-actions
autofocus
- data-qa-selector="description_field"
@input="$emit('input', $event)"
@keydown.meta.enter="saveIssuable"
@keydown.ctrl.enter="saveIssuable"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index dee4c536afa..32df19dfe44 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -1,17 +1,17 @@
<script>
import {
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlDropdownDivider,
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLink,
GlModal,
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { STATUS_CLOSED, TYPE_ISSUE, issuableTypeText } from '~/issues/constants';
@@ -59,9 +59,9 @@ export default {
components: {
DeleteIssueModal,
GlButton,
- GlDropdown,
+ GlDisclosureDropdown,
GlDropdownDivider,
- GlDropdownItem,
+ GlDisclosureDropdownItem,
GlLink,
GlModal,
AbuseCategorySelector,
@@ -184,6 +184,18 @@ export default {
showMovedSidebarOptions() {
return this.isMrSidebarMoved && this.isUserSignedIn;
},
+ newIssueItem() {
+ return {
+ text: this.newIssueTypeText,
+ href: this.newIssuePath,
+ };
+ },
+ submitSpamItem() {
+ return {
+ text: __('Submit as spam'),
+ href: this.submitAsSpamPath,
+ };
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -197,6 +209,7 @@ export default {
toggleIssueState() {
if (!this.isClosed && this.getBlockedByIssues?.length) {
this.$refs.blockedByIssuesModal.show();
+ this.closeActionsDropdown();
return;
}
@@ -204,6 +217,7 @@ export default {
},
toggleReportAbuseDrawer(isOpen) {
this.isReportAbuseDrawerOpen = isOpen;
+ this.closeActionsDropdown();
},
invokeUpdateIssueMutation() {
this.toggleStateButtonLoading(true);
@@ -237,6 +251,7 @@ export default {
.catch(() => createAlert({ message: __('Error occurred while updating the issue status') }))
.finally(() => {
this.toggleStateButtonLoading(false);
+ this.closeActionsDropdown();
});
},
promoteToEpic() {
@@ -267,16 +282,24 @@ export default {
.catch(() => createAlert({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.toggleStateButtonLoading(false);
+ this.closeActionsDropdown();
});
},
edit() {
issuesEventHub.$emit('open.form');
+ this.closeActionsDropdown();
},
copyReference() {
toast(__('Reference copied'));
+ this.closeActionsDropdown();
},
copyEmailAddress() {
toast(__('Email address copied'));
+ this.closeActionsDropdown();
+ },
+ closeActionsDropdown() {
+ this.$refs.issuableActionsDropdownMobile?.close();
+ this.$refs.issuableActionsDropdownDesktop?.close();
},
},
TYPE_ISSUE,
@@ -285,87 +308,90 @@ export default {
<template>
<div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-sm-gap-3">
- <gl-dropdown
- v-if="hasMobileDropdown"
- class="gl-sm-display-none! w-100"
- block
- :text="dropdownText"
- data-testid="mobile-dropdown"
- :loading="isToggleStateButtonLoading"
- >
- <template v-if="showMovedSidebarOptions">
- <sidebar-subscriptions-widget
- :iid="String(iid)"
- :full-path="fullPath"
- :issuable-type="$options.TYPE_ISSUE"
- data-testid="notification-toggle"
- />
+ <div class="gl-sm-display-none! w-100">
+ <gl-disclosure-dropdown
+ v-if="hasMobileDropdown"
+ ref="issuableActionsDropdownMobile"
+ toggle-class="gl-w-full"
+ block
+ :toggle-text="dropdownText"
+ :auto-close="false"
+ data-testid="mobile-dropdown"
+ :loading="isToggleStateButtonLoading"
+ placement="right"
+ >
+ <template v-if="showMovedSidebarOptions">
+ <sidebar-subscriptions-widget
+ :iid="String(iid)"
+ :full-path="fullPath"
+ :issuable-type="$options.TYPE_ISSUE"
+ data-testid="notification-toggle"
+ />
- <gl-dropdown-divider />
- </template>
+ <gl-dropdown-divider />
+ </template>
- <template v-if="showLockIssueOption">
- <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
- </template>
+ <template v-if="showLockIssueOption">
+ <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
+ </template>
- <gl-dropdown-item v-if="canUpdateIssue" @click="edit">
- {{ $options.i18n.edit }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="showToggleIssueStateButton"
- :data-testid="`mobile_${qaSelector}`"
- @click="toggleIssueState"
- >
- {{ buttonText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath">
- {{ newIssueTypeText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
- {{ __('Promote to epic') }}
- </gl-dropdown-item>
- <template v-if="isMrSidebarMoved">
- <gl-dropdown-item
- :data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
- data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
- >
- <gl-dropdown-item
- v-if="issuableEmailAddress && showMovedSidebarOptions"
- :data-clipboard-text="issuableEmailAddress"
- data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
+ <gl-disclosure-dropdown-item v-if="canUpdateIssue" @action="edit">
+ <template #list-item>{{ $options.i18n.edit }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="showToggleIssueStateButton"
+ :data-testid="`mobile_${qaSelector}`"
+ @action="toggleIssueState"
>
- </template>
- <gl-dropdown-item
- v-if="canReportSpam"
- :href="submitAsSpamPath"
- data-method="post"
- rel="nofollow"
- >
- {{ __('Submit as spam') }}
- </gl-dropdown-item>
- <template v-if="canDestroyIssue">
- <gl-dropdown-divider />
- <gl-dropdown-item
- v-gl-modal="$options.deleteModalId"
- variant="danger"
- @click="track('click_dropdown')"
+ <template #list-item>{{ buttonText }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item v-if="canCreateIssue" :item="newIssueItem" />
+ <gl-disclosure-dropdown-item v-if="canPromoteToEpic" @action="promoteToEpic">
+ <template #list-item>{{ __('Promote to epic') }}</template>
+ </gl-disclosure-dropdown-item>
+ <template v-if="isMrSidebarMoved">
+ <gl-disclosure-dropdown-item
+ :data-clipboard-text="issuableReference"
+ button-class="js-copy-reference"
+ data-testid="copy-reference"
+ @action="copyReference"
+ ><template #list-item>{{
+ $options.i18n.copyReferenceText
+ }}</template></gl-disclosure-dropdown-item
+ >
+ <gl-disclosure-dropdown-item
+ v-if="issuableEmailAddress && showMovedSidebarOptions"
+ :data-clipboard-text="issuableEmailAddress"
+ data-testid="copy-email"
+ @action="copyEmailAddress"
+ >{{ copyMailAddressText }}</gl-disclosure-dropdown-item
+ >
+ </template>
+ <gl-disclosure-dropdown-item
+ v-if="canReportSpam"
+ :item="submitSpamItem"
+ data-method="post"
+ rel="nofollow"
+ />
+ <template v-if="canDestroyIssue">
+ <gl-dropdown-divider />
+ <gl-disclosure-dropdown-item
+ v-gl-modal="$options.deleteModalId"
+ variant="danger"
+ @action="track('click_dropdown')"
+ >
+ <template #list-item>{{ deleteButtonText }}</template>
+ </gl-disclosure-dropdown-item>
+ </template>
+ <gl-disclosure-dropdown-item
+ v-if="!isIssueAuthor && isUserSignedIn"
+ data-testid="report-abuse-item"
+ @action="toggleReportAbuseDrawer(true)"
>
- {{ deleteButtonText }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-item
- v-if="!isIssueAuthor && isUserSignedIn"
- data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
- >
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>{{ $options.i18n.reportAbuse }}</template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
+ </div>
<gl-button
v-if="canUpdateIssue"
@@ -379,20 +405,22 @@ export default {
{{ $options.i18n.edit }}
</gl-button>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="hasDesktopDropdown"
id="new-actions-header-dropdown"
+ ref="issuableActionsDropdownDesktop"
v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex!"
icon="ellipsis_v"
category="tertiary"
- :text="dropdownText"
- :text-sr-only="true"
+ placement="left"
+ :toggle-text="dropdownText"
+ text-sr-only
:title="dropdownText"
:aria-label="dropdownText"
+ :auto-close="false"
data-testid="desktop-dropdown"
no-caret
- right
>
<template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons">
<sidebar-subscriptions-widget
@@ -401,73 +429,70 @@ export default {
:issuable-type="$options.TYPE_ISSUE"
data-testid="notification-toggle"
/>
-
<gl-dropdown-divider />
</template>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="showToggleIssueStateButton"
data-testid="toggle-issue-state-button"
- @click="toggleIssueState"
+ @action="toggleIssueState"
>
- {{ buttonText }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="canCreateIssue && isUserSignedIn" :href="newIssuePath">
- {{ newIssueTypeText }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ <template #list-item>{{ buttonText }}</template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item v-if="canCreateIssue && isUserSignedIn" :item="newIssueItem" />
+ <gl-disclosure-dropdown-item
v-if="canPromoteToEpic"
:disabled="isToggleStateButtonLoading"
data-testid="promote-button"
- @click="promoteToEpic"
+ @action="promoteToEpic"
>
- {{ __('Promote to epic') }}
- </gl-dropdown-item>
+ <template #list-item>{{ __('Promote to epic') }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="showLockIssueOption">
<issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" />
</template>
<template v-if="isMrSidebarMoved">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
button-class="js-copy-reference"
data-testid="copy-reference"
- @click="copyReference"
- >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item
+ @action="copyReference"
+ ><template #list-item>{{
+ $options.i18n.copyReferenceText
+ }}</template></gl-disclosure-dropdown-item
>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="issuableEmailAddress && showMovedSidebarOptions"
:data-clipboard-text="issuableEmailAddress"
data-testid="copy-email"
- @click="copyEmailAddress"
- >{{ copyMailAddressText }}</gl-dropdown-item
+ @action="copyEmailAddress"
+ ><template #list-item>{{ copyMailAddressText }}</template></gl-disclosure-dropdown-item
>
</template>
<gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-if="canReportSpam"
- :href="submitAsSpamPath"
+ :item="submitSpamItem"
data-method="post"
rel="nofollow"
- >
- {{ __('Submit as spam') }}
- </gl-dropdown-item>
- <gl-dropdown-item
+ />
+ <gl-disclosure-dropdown-item
v-if="!isIssueAuthor && isUserSignedIn"
data-testid="report-abuse-item"
- @click="toggleReportAbuseDrawer(true)"
+ @action="toggleReportAbuseDrawer(true)"
>
- {{ $options.i18n.reportAbuse }}
- </gl-dropdown-item>
+ <template #list-item>{{ $options.i18n.reportAbuse }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="canDestroyIssue">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
v-gl-modal="$options.deleteModalId"
variant="danger"
data-testid="delete-issue-button"
- @click="track('click_dropdown')"
+ @action="track('click_dropdown')"
>
- {{ deleteButtonText }}
- </gl-dropdown-item>
+ <template #list-item>{{ deleteButtonText }}</template>
+ </gl-disclosure-dropdown-item>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
<gl-modal
ref="blockedByIssuesModal"
diff --git a/app/assets/javascripts/issues/show/components/issue_header.vue b/app/assets/javascripts/issues/show/components/issue_header.vue
index 211f3217ddc..96eb8fbb3c7 100644
--- a/app/assets/javascripts/issues/show/components/issue_header.vue
+++ b/app/assets/javascripts/issues/show/components/issue_header.vue
@@ -82,7 +82,7 @@ export default {
return this.issuableState === STATUS_OPEN || this.issuableState === STATUS_REOPENED;
},
statusIcon() {
- return this.isOpen ? 'issues' : 'issue-closed';
+ return this.isOpen ? 'issue-open-m' : 'issue-close';
},
statusText() {
if (this.isOpen) {
@@ -115,11 +115,9 @@ export default {
<template #status-badge>
<gl-sprintf v-if="closedStatusLink" :message="statusText">
<template #link>
- <gl-link
- class="gl-reset-color! gl-reset-font-size gl-text-decoration-underline"
- :href="closedStatusLink"
- >{{ closedStatusText }}</gl-link
- >
+ <gl-link class="gl-reset-color! gl-text-decoration-underline" :href="closedStatusLink">{{
+ closedStatusText
+ }}</gl-link>
</template>
</gl-sprintf>
<template v-else>{{ statusText }}</template>
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
index 738bb2c2aa0..18e37c4216c 100644
--- a/app/assets/javascripts/issues/show/components/sticky_header.vue
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -2,12 +2,7 @@
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 { issuableStatusText, STATUS_CLOSED, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
export default {
@@ -60,10 +55,7 @@ export default {
return this.issuableStatus === STATUS_CLOSED;
},
statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
+ return this.isClosed ? 'issue-close' : 'issue-open-m';
},
statusText() {
return issuableStatusText[this.issuableStatus];
@@ -84,7 +76,7 @@ export default {
data-testid="issue-sticky-header"
>
<div
- class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto gl-px-5"
+ class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-gap-2 gl-mx-auto"
>
<gl-badge :variant="statusVariant">
<gl-icon :name="statusIcon" />
diff --git a/app/assets/javascripts/issues/show/utils/parse_data.js b/app/assets/javascripts/issues/show/utils/parse_data.js
index f1e6bd2419a..23d5292da00 100644
--- a/app/assets/javascripts/issues/show/utils/parse_data.js
+++ b/app/assets/javascripts/issues/show/utils/parse_data.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { sanitize } from '~/lib/dompurify';
// We currently load + parse the data from the issue app and related merge request
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 1a10360ed30..85e250b14a0 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -37,9 +37,15 @@ export const I18N_OAUTH_FAILED_MESSAGE = s__(
export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_panel', {
anchor: 'use-the-integration',
});
+export const PREREQUISITES_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'prerequisites',
+});
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'set-up-oauth-authentication',
});
+export const SET_UP_INSTANCE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
+ anchor: 'set-up-your-instance',
+});
export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'failed-to-update-the-gitlab-instance',
});
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
index d8d2db18d9f..9f8fae5b476 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
@@ -1,13 +1,44 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
-import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+import { GlButton, GlFormCheckbox, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ PREREQUISITES_DOC_LINK,
+ OAUTH_SELF_MANAGED_DOC_LINK,
+ SET_UP_INSTANCE_DOC_LINK,
+} from '~/jira_connect/subscriptions/constants';
export default {
components: {
GlButton,
+ GlFormCheckbox,
GlLink,
},
- OAUTH_SELF_MANAGED_DOC_LINK,
+ data() {
+ return {
+ requiredSteps: [
+ {
+ name: s__('JiraConnect|Prerequisites'),
+ link: PREREQUISITES_DOC_LINK,
+ checked: false,
+ },
+ {
+ name: s__('JiraConnect|Set up OAuth authentication'),
+ link: OAUTH_SELF_MANAGED_DOC_LINK,
+ checked: false,
+ },
+ {
+ name: s__('JiraConnect|Set up your instance'),
+ link: SET_UP_INSTANCE_DOC_LINK,
+ checked: false,
+ },
+ ],
+ };
+ },
+ computed: {
+ nextDisabled() {
+ return !this.requiredSteps.every((step) => step.checked);
+ },
+ },
};
</script>
@@ -17,20 +48,25 @@ export default {
<p>
{{
s__(
- 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab.',
+ 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab:',
)
}}
- <gl-link
- class="gl-reset-font-size!"
- :href="$options.OAUTH_SELF_MANAGED_DOC_LINK"
- target="_blank"
- >{{ __('Learn more') }}</gl-link
- >
</p>
+ <div class="gl-mb-5">
+ <div v-for="step in requiredSteps" :key="step.name" class="gl-mb-2">
+ <gl-form-checkbox v-model="step.checked">
+ <gl-link :href="step.link" target="_blank">
+ {{ step.name }}
+ </gl-link>
+ </gl-form-checkbox>
+ </div>
+ </div>
<div class="gl-display-flex gl-justify-content-space-between">
<gl-button @click="$emit('back')">{{ __('Back') }}</gl-button>
- <gl-button variant="confirm" @click="$emit('next')">{{ __('Next') }}</gl-button>
+ <gl-button variant="confirm" :disabled="nextDisabled" @click="$emit('next')"
+ >{{ __('Next') }}
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 6ab530576fc..5285fa363a5 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,5 +1,4 @@
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
-import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
import { persistCache } from 'apollo3-cache-persist';
import ActionCableLink from '~/actioncable_link';
@@ -116,18 +115,14 @@ Object.defineProperty(window, 'pendingApolloRequests', {
function createApolloClient(resolvers = {}, config = {}) {
const {
baseUrl,
- batchMax = 10,
cacheConfig = { typePolicies: {}, possibleTypes: {} },
fetchPolicy = fetchPolicies.CACHE_FIRST,
typeDefs,
httpHeaders = {},
fetchCredentials = 'same-origin',
path = '/api/graphql',
- useGet = false,
} = config;
- const shouldUnbatch = gon.features?.unbatchGraphqlQueries;
-
let ac = null;
let uri = `${gon.relative_url_root || ''}${path}`;
@@ -146,7 +141,6 @@ function createApolloClient(resolvers = {}, config = {}) {
// We set to `same-origin` which is default value in modern browsers.
// See https://github.com/whatwg/fetch/pull/585 for more information.
credentials: fetchCredentials,
- batchMax,
};
/*
@@ -165,14 +159,10 @@ function createApolloClient(resolvers = {}, config = {}) {
return fetch(stripWhitespaceFromQuery(url, uri), options);
};
- const requestLink = ApolloLink.split(
- () => useGet || shouldUnbatch,
- new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
- new BatchHttpLink(httpOptions),
- );
+ const requestLink = new HttpLink({ ...httpOptions, fetch: fetchIntervention });
const uploadsLink = ApolloLink.split(
- (operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
+ (operation) => operation.getContext().hasUpload,
createUploadLink(httpOptions),
);
diff --git a/app/assets/javascripts/lib/utils/color_utils.js b/app/assets/javascripts/lib/utils/color_utils.js
index a9f4257e28b..74c9f7de8c1 100644
--- a/app/assets/javascripts/lib/utils/color_utils.js
+++ b/app/assets/javascripts/lib/utils/color_utils.js
@@ -46,5 +46,5 @@ export function darkModeEnabled() {
if (isWebIde) {
return ideDarkThemes.includes(window.gon?.user_color_scheme);
}
- return document.body.classList.contains('gl-dark');
+ return document.documentElement.classList.contains('gl-dark');
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 7d16af003e4..27da2ac6ce1 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -7,6 +7,7 @@ import $ from 'jquery';
import { isFunction, defer, escape, partial, toLower } from 'lodash';
import Cookies from '~/lib/utils/cookies';
import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
+import { DEFAULT_CI_CONFIG_PATH, CI_CONFIG_PATH_EXTENSION } from '~/lib/utils/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
@@ -737,3 +738,17 @@ export const isCurrentUser = (userId) => {
export const cloneWithoutReferences = (obj) => {
return JSON.parse(JSON.stringify(obj));
};
+
+/**
+ * Returns true if the given path is the default CI config path.
+ */
+export const isDefaultCiConfig = (path) => {
+ return path === DEFAULT_CI_CONFIG_PATH;
+};
+
+/**
+ * Returns true if the given path has the CI config path extension.
+ */
+export const hasCiConfigExtension = (path) => {
+ return CI_CONFIG_PATH_EXTENSION.test(path);
+};
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index da5fb831ae5..d9ac0abf7b3 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -23,3 +23,6 @@ export const BYTES_FORMAT_BYTES = 'B';
export const BYTES_FORMAT_KIB = 'KiB';
export const BYTES_FORMAT_MIB = 'MiB';
export const BYTES_FORMAT_GIB = 'GiB';
+
+export const DEFAULT_CI_CONFIG_PATH = '.gitlab-ci.yml';
+export const CI_CONFIG_PATH_EXTENSION = /(\.gitlab-ci\.yml)/;
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index a973cd890ba..89170ecc55d 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -108,15 +108,27 @@ timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration());
const setupAbsoluteFormatters = () => {
- const cache = {};
+ let cache = {};
// Intl.DateTimeFormat options (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options)
+ // For hourCycle please check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
+ const hourCycle = [undefined, 'h12', 'h23'];
const formats = {
- [DATE_WITH_TIME_FORMAT]: () => ({ dateStyle: 'medium', timeStyle: 'short' }),
+ [DATE_WITH_TIME_FORMAT]: () => ({
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ hourCycle: hourCycle[window.gon?.time_display_format || 0],
+ }),
[DATE_ONLY_FORMAT]: () => ({ dateStyle: 'medium' }),
};
return (formatName = DEFAULT_DATE_TIME_FORMAT) => {
+ if (cache.time_display_format !== window.gon?.time_display_format) {
+ cache = {
+ time_display_format: window.gon?.time_display_format,
+ };
+ }
+
if (cache[formatName]) {
return cache[formatName];
}
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index 652ae337506..6713a18cbf3 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -69,18 +69,32 @@ export const isIntegerGreaterThan = (value, greaterThan) =>
isParseableAsInteger(value) && parseInt(value, 10) > greaterThan;
/**
- * Regexp that matches email structure.
+ * Regexp that matches service desk setting email structure.
* Taken from app/models/service_desk_setting.rb custom_email
*/
-export const EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
+const SERVICE_DESK_SETTING_EMAIL_REGEXP = /^[\w\-._]+@[\w\-.]+\.[a-zA-Z]{2,}$/;
/**
- * Checks if the input is a valid email address
+ * Checks if the input is a valid service desk setting email address
*
* @param {String} - value
* @returns {Boolean}
*/
-export const isEmail = (value) => EMAIL_REGEXP.test(value);
+export const isServiceDeskSettingEmail = (value) => SERVICE_DESK_SETTING_EMAIL_REGEXP.test(value);
+
+/**
+ * Regexp that matches user email structure.
+ * Taken from DeviseEmailValidator
+ */
+const USER_EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/;
+
+/**
+ * Checks if the input is a valid user email address
+ *
+ * @param {String} - value
+ * @returns {Boolean}
+ */
+export const isUserEmail = (value) => USER_EMAIL_REGEXP.test(value);
/**
* A form object serializer
diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js
index 7cfcd11ece9..e5022551b97 100644
--- a/app/assets/javascripts/lib/utils/keys.js
+++ b/app/assets/javascripts/lib/utils/keys.js
@@ -1,5 +1,6 @@
export const ESC_KEY = 'Escape';
export const ENTER_KEY = 'Enter';
+export const NUMPAD_ENTER_KEY = 'NumpadEnter';
export const BACKSPACE_KEY = 'Backspace';
export const ARROW_DOWN_KEY = 'ArrowDown';
export const ARROW_UP_KEY = 'ArrowUp';
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 5bfdd174694..29189e3ac2f 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -29,7 +29,6 @@ import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
import { initSidebarTracking } from './pages/shared/nav/sidebar_tracking';
-import initServicePingConsent from './service_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@@ -93,7 +92,6 @@ function deferredInitialisation() {
initBreadcrumbs();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
- initServicePingConsent();
initUserPopovers();
initBroadcastNotifications();
initPersistentUserCallouts();
diff --git a/app/assets/javascripts/members/components/avatars/group_avatar.vue b/app/assets/javascripts/members/components/avatars/group_avatar.vue
index 3b176bf2b43..83b5855492b 100644
--- a/app/assets/javascripts/members/components/avatars/group_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/group_avatar.vue
@@ -1,11 +1,18 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatarLabeled, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import PrivateIcon from '../icons/private_icon.vue';
import { AVATAR_SIZE } from '../../constants';
export default {
name: 'GroupAvatar',
- avatarSize: AVATAR_SIZE,
- components: { GlAvatarLink, GlAvatarLabeled },
+ components: { GlAvatarLink, GlAvatarLabeled, PrivateIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ private: __('Private'),
+ },
props: {
member: {
type: Object,
@@ -16,19 +23,36 @@ export default {
group() {
return this.member.sharedWithGroup;
},
+ isPrivate() {
+ return this.member.isSharedWithGroupPrivate;
+ },
+ avatarLabeledProps() {
+ const label = this.isPrivate ? this.$options.i18n.private : this.group.fullName;
+
+ return {
+ label,
+ src: this.group.avatarUrl,
+ alt: label,
+ size: AVATAR_SIZE,
+ entityName: this.isPrivate ? this.$options.i18n.private : this.group.name,
+ entityId: this.group.id,
+ };
+ },
},
};
</script>
<template>
- <gl-avatar-link :href="group.webUrl">
- <gl-avatar-labeled
- :label="group.fullName"
- :src="group.avatarUrl"
- :alt="group.fullName"
- :size="$options.avatarSize"
- :entity-name="group.name"
- :entity-id="group.id"
- />
+ <div v-if="isPrivate">
+ <gl-avatar-labeled v-bind="avatarLabeledProps">
+ <template #meta>
+ <div class="gl-p-1">
+ <private-icon />
+ </div>
+ </template>
+ </gl-avatar-labeled>
+ </div>
+ <gl-avatar-link v-else :href="group.webUrl">
+ <gl-avatar-labeled v-bind="avatarLabeledProps" />
</gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/members/components/icons/private_icon.vue b/app/assets/javascripts/members/components/icons/private_icon.vue
new file mode 100644
index 00000000000..6168ea955f3
--- /dev/null
+++ b/app/assets/javascripts/members/components/icons/private_icon.vue
@@ -0,0 +1,19 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'GroupAvatar',
+ components: { GlIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ tooltip: s__('Members|Private group information is only accessible to its members.'),
+ },
+};
+</script>
+
+<template>
+ <gl-icon v-gl-tooltip="$options.i18n.tooltip" name="eye-slash" />
+</template>
diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index ed1971d020b..f1a1c4cecaa 100644
--- a/app/assets/javascripts/members/components/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
@@ -1,10 +1,12 @@
<script>
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import PrivateIcon from '../icons/private_icon.vue';
export default {
name: 'MemberSource',
i18n: {
+ private: __('Private'),
inherited: __('Inherited'),
directMember: __('Direct member'),
directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'),
@@ -13,16 +15,24 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- components: { GlSprintf },
+ components: { GlSprintf, PrivateIcon },
props: {
memberSource: {
type: Object,
- required: true,
+ required: false,
+ default() {
+ return {};
+ },
},
isDirectMember: {
type: Boolean,
required: true,
},
+ isSharedWithGroupPrivate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
createdBy: {
type: Object,
required: false,
@@ -43,7 +53,11 @@ export default {
</script>
<template>
- <span v-if="showCreatedBy">
+ <div v-if="isSharedWithGroupPrivate" class="gl-display-flex gl-column-gap-2">
+ <span>{{ $options.i18n.private }}</span>
+ <private-icon />
+ </div>
+ <span v-else-if="showCreatedBy">
<gl-sprintf :message="messageWithCreatedBy">
<template #group>
<a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 68f624e9a3d..2b3294c1c79 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -270,6 +270,7 @@ export default {
:is-direct-member="isDirectMember"
:member-source="member.source"
:created-by="member.createdBy"
+ :is-shared-with-group-private="member.isSharedWithGroupPrivate"
/>
</members-table-cell>
</template>
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 4b39c000b8f..2b72a3fe6e8 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -3,9 +3,10 @@ import { GlCollapsibleListbox } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
-import * as Sentry from '@sentry/browser';
-import { s__ } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
+import { roleDropdownItems, initialSelectedRole } from 'ee_else_ce/members/utils';
+import { s__ } from '~/locale';
export default {
name: 'RoleDropdown',
@@ -29,7 +30,7 @@ export default {
return {
isDesktop: false,
busy: false,
- selectedRoleValue: this.member.accessLevel.integerValue,
+ selectedRole: null,
};
},
computed: {
@@ -37,12 +38,12 @@ export default {
return this.permissions.canOverride && !this.member.isOverridden;
},
dropdownItems() {
- return Object.entries(this.member.validRoles).map(([name, value]) => ({
- value,
- text: name,
- }));
+ return roleDropdownItems(this.member);
},
},
+ created() {
+ this.selectedRole = initialSelectedRole(this.dropdownItems.flatten, this.member);
+ },
mounted() {
this.isDesktop = bp.isDesktop();
},
@@ -52,44 +53,39 @@ export default {
return dispatch(`${this.namespace}/updateMemberRole`, payload);
},
}),
- async handleOverageConfirm(currentRoleValue, newRoleValue, newRoleName) {
- return guestOverageConfirmAction({
- currentRoleValue,
- newRoleValue,
- newRoleName,
- group: this.group,
- memberId: this.member.id,
- memberType: this.namespace,
- });
- },
- async handleSelect(newRoleValue) {
- const currentRoleValue = this.member.accessLevel.integerValue;
- if (newRoleValue === currentRoleValue) {
- return;
- }
-
+ async handleSelect(value) {
this.busy = true;
- const { text: newRoleName } = this.dropdownItems.find((item) => item.value === newRoleValue);
- const confirmed = await this.handleOverageConfirm(
- currentRoleValue,
- newRoleValue,
- newRoleName,
- );
- if (!confirmed) {
- this.selectedRoleValue = currentRoleValue;
- this.busy = false;
- return;
- }
+ const newRole = this.dropdownItems.flatten.find((item) => item.value === value);
+ const previousRole = this.selectedRole;
try {
+ const confirmed = await guestOverageConfirmAction({
+ currentRoleValue: this.member.accessLevel.integerValue,
+ newRoleValue: newRole.accessLevel,
+ newRoleName: newRole.text,
+ newMemberRoleId: newRole.memberRoleId,
+ group: this.group,
+ memberId: this.member.id,
+ memberType: this.namespace,
+ });
+ if (!confirmed) {
+ return;
+ }
+
+ this.selectedRole = value;
+
await this.updateMemberRole({
memberId: this.member.id,
- accessLevel: { integerValue: newRoleValue, stringValue: newRoleName },
+ accessLevel: {
+ integerValue: newRole.accessLevel,
+ memberRoleId: newRole.memberRoleId,
+ },
});
this.$toast.show(s__('Members|Role updated successfully.'));
} catch (error) {
+ this.selectedRole = previousRole;
Sentry.captureException(error);
} finally {
this.busy = false;
@@ -101,14 +97,14 @@ export default {
<template>
<gl-collapsible-listbox
- v-model="selectedRoleValue"
:placement="isDesktop ? 'left' : 'right'"
:toggle-text="member.accessLevel.stringValue"
:header-text="__('Change role')"
:disabled="disabled"
:loading="busy"
data-qa-selector="access_level_dropdown"
- :items="dropdownItems"
+ :items="dropdownItems.formatted"
+ :selected="selectedRole"
@select="handleSelect"
>
<template #list-item="{ item }">
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index 712f0d6caa7..d696f618a3c 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -6,7 +6,10 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve
try {
await axios.put(
state.memberPath.replace(/:id$/, memberId),
- state.requestFormatter({ accessLevel: accessLevel.integerValue }),
+ state.requestFormatter({
+ accessLevel: accessLevel.integerValue,
+ memberRoleId: accessLevel.memberRoleId,
+ }),
);
commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 09e4b5e8a6f..1304fb0fee1 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,4 +1,4 @@
-import { isUndefined } from 'lodash';
+import { isUndefined, uniqueId } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams } from '~/lib/utils/url_utility';
import {
@@ -35,6 +35,36 @@ export const generateBadges = ({ member, isCurrentUser, canManageMembers }) => [
},
];
+/**
+ * Creates the dropdowns options for static roles
+ *
+ * @param {object} member
+ * @param {Map<string, number>} member.validRoles
+ */
+export const roleDropdownItems = ({ validRoles }) => {
+ const staticRoleDropdownItems = Object.entries(validRoles).map(([name, value]) => ({
+ accessLevel: value,
+ memberRoleId: null, // The value `null` is need to downgrade from custom role to static role. See: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133430#note_1595153555
+ text: name,
+ value: uniqueId('role-static-'),
+ }));
+
+ return { flatten: staticRoleDropdownItems, formatted: staticRoleDropdownItems };
+};
+
+/**
+ * Finds and returns unique value
+ *
+ * @param {Array<{accessLevel: number, memberRoleId: null, text: string, value: string}>} flattenDropdownItems
+ * @param {object} member
+ * @param {{integerValue: number}} member.accessLevel
+ */
+export const initialSelectedRole = (flattenDropdownItems, member) => {
+ return flattenDropdownItems.find(
+ ({ accessLevel }) => accessLevel === member.accessLevel.integerValue,
+ )?.value;
+};
+
export const isGroup = (member) => {
return Boolean(member.sharedWithGroup);
};
@@ -128,6 +158,7 @@ export const parseDataAttributes = (el) => {
export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName) => ({
accessLevel,
+ memberRoleId,
...otherProperties
}) => {
const accessLevelProperty = !isUndefined(accessLevel)
@@ -137,6 +168,7 @@ export const baseRequestFormatter = (basePropertyName, accessLevelPropertyName)
return {
[basePropertyName]: {
...accessLevelProperty,
+ member_role_id: memberRoleId ?? null,
...otherProperties,
},
};
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 2095f24eb84..8ea995b8b4e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -21,12 +21,7 @@ import syntaxHighlight from './syntax_highlight';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(
- {},
- {
- useGet: true,
- },
- ),
+ defaultClient: createDefaultClient(),
});
// MergeRequestTabs
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index e8bdb854334..877e6142bae 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -1,5 +1,12 @@
<script>
-import { GlIntersectionObserver, GlLink, GlSprintf, GlBadge } from '@gitlab/ui';
+import {
+ GlIntersectionObserver,
+ GlLink,
+ GlSprintf,
+ GlBadge,
+ GlIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -45,6 +52,7 @@ export default {
GlLink,
GlSprintf,
GlBadge,
+ GlIcon,
DiscussionCounter,
StatusBadge,
TodoWidget,
@@ -53,10 +61,12 @@ export default {
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
inject: {
projectPath: { default: null },
+ sourceProjectPath: { default: null },
title: { default: '' },
tabs: { default: () => [] },
isFluidLayout: { default: false },
@@ -89,6 +99,16 @@ export default {
isNotificationsTodosButtons() {
return this.glFeatures.notificationsTodosButtons && this.glFeatures.movedMrSidebar;
},
+ isForked() {
+ return this.projectPath !== this.sourceProjectPath;
+ },
+ sourceBranch() {
+ if (this.isForked) {
+ return `${this.sourceProjectPath}:${this.getNoteableData.source_branch}`;
+ }
+
+ return this.getNoteableData.source_branch;
+ },
},
watch: {
discussionTabCounter(val) {
@@ -122,8 +142,8 @@ export default {
:class="{ 'gl-visibility-hidden': !isStickyHeaderVisible }"
>
<div
- class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-px-5 gl-w-full"
- :class="{ 'gl-max-w-container-xl': !isFluidLayout }"
+ class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-w-full"
+ :class="{ 'container-limited': !isFluidLayout }"
>
<div class="gl-w-full gl-display-flex gl-align-items-baseline">
<status-badge
@@ -153,8 +173,17 @@ export default {
:title="getNoteableData.source_branch"
:href="getNoteableData.source_branch_path"
class="gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-text-truncate gl-max-w-26"
+ data-testid="source-branch"
>
- {{ getNoteableData.source_branch }}
+ <span
+ v-if="isForked"
+ v-gl-tooltip
+ class="gl-vertical-align-middle gl-mr-n2"
+ :title="__('The source project is a fork')"
+ >
+ <gl-icon name="fork" :size="12" class="gl-ml-1" />
+ </span>
+ {{ sourceBranch }}
</gl-link>
</template>
<template #target>
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index 2f7fb542d0e..a5e306b5372 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -1,19 +1,10 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIcon,
-} from '@gitlab/ui';
+import { GlBadge, GlButton, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce, isEqual } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__, __, sprintf } from '~/locale';
import createStore from '../stores';
-import MilestoneResultsSection from './milestone_results_section.vue';
const SEARCH_DEBOUNCE_MS = 250;
@@ -21,14 +12,9 @@ export default {
name: 'MilestoneCombobox',
store: createStore(),
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlIcon,
- MilestoneResultsSection,
+ GlCollapsibleListbox,
+ GlBadge,
+ GlButton,
},
props: {
value: {
@@ -56,27 +42,43 @@ export default {
required: false,
},
},
- data() {
- return {
- searchQuery: '',
- };
- },
translations: {
- milestone: s__('MilestoneCombobox|Milestone'),
selectMilestone: s__('MilestoneCombobox|Select milestone'),
noMilestone: s__('MilestoneCombobox|No milestone'),
- noResultsLabel: s__('MilestoneCombobox|No matching results'),
- searchMilestones: s__('MilestoneCombobox|Search Milestones'),
- searchErrorMessage: s__('MilestoneCombobox|An error occurred while searching for milestones'),
projectMilestones: s__('MilestoneCombobox|Project milestones'),
groupMilestones: s__('MilestoneCombobox|Group milestones'),
+ unselect: __('Unselect'),
},
computed: {
...mapState(['matches', 'selectedMilestones']),
- ...mapGetters(['isLoading', 'groupMilestonesEnabled']),
+ ...mapGetters(['isLoading']),
+ allMilestones() {
+ const { groupMilestones, projectMilestones } = this.matches || {};
+ const milestones = [];
+
+ if (projectMilestones?.totalCount) {
+ milestones.push({
+ id: 'project-milestones',
+ text: this.$options.translations.projectMilestones,
+ options: projectMilestones.list,
+ totalCount: projectMilestones.totalCount,
+ });
+ }
+
+ if (groupMilestones?.totalCount) {
+ milestones.push({
+ id: 'group-milestones',
+ text: this.$options.translations.groupMilestones,
+ options: groupMilestones.list,
+ totalCount: groupMilestones.totalCount,
+ });
+ }
+
+ return milestones;
+ },
selectedMilestonesLabel() {
const { selectedMilestones } = this;
- const firstMilestoneName = selectedMilestones[0];
+ const [firstMilestoneName] = selectedMilestones;
if (selectedMilestones.length === 0) {
return this.$options.translations.noMilestone;
@@ -92,20 +94,6 @@ export default {
numberOfOtherMilestones,
});
},
- showProjectMilestoneSection() {
- return Boolean(
- this.matches.projectMilestones.totalCount > 0 || this.matches.projectMilestones.error,
- );
- },
- showGroupMilestoneSection() {
- return (
- this.groupMilestonesEnabled &&
- Boolean(this.matches.groupMilestones.totalCount > 0 || this.matches.groupMilestones.error)
- );
- },
- showNoResults() {
- return !this.showProjectMilestoneSection && !this.showGroupMilestoneSection;
- },
},
watch: {
// Keep the Vuex store synchronized if the parent
@@ -127,8 +115,8 @@ export default {
// because we need to access the .cancel() method
// lodash attaches to the function, which is
// made inaccessible by Vue.
- this.debouncedSearch = debounce(function search() {
- this.search(this.searchQuery);
+ this.debouncedSearch = debounce(function search(q) {
+ this.search(q);
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
@@ -143,22 +131,14 @@ export default {
'setGroupMilestonesAvailable',
'setSelectedMilestones',
'clearSelectedMilestones',
- 'toggleMilestones',
'search',
'fetchMilestones',
]),
- focusSearchBox() {
- this.$refs.searchBox.$el.querySelector('input').focus();
- },
- onSearchBoxEnter() {
- this.debouncedSearch.cancel();
- this.search(this.searchQuery);
+ onSearchBoxInput(q) {
+ this.debouncedSearch(q);
},
- onSearchBoxInput() {
- this.debouncedSearch();
- },
- selectMilestone(milestone) {
- this.toggleMilestones(milestone);
+ selectMilestone(milestones) {
+ this.setSelectedMilestones(milestones);
this.$emit('input', this.selectedMilestones);
},
selectNoMilestone() {
@@ -170,84 +150,42 @@ export default {
</script>
<template>
- <gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox">
- <template #button-content>
- <span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{
- selectedMilestonesLabel
- }}</span>
- <gl-icon name="chevron-down" />
- </template>
-
- <gl-dropdown-section-header>
- <span class="text-center d-block">{{ $options.translations.selectMilestone }}</span>
- </gl-dropdown-section-header>
-
- <gl-dropdown-divider />
-
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="searchQuery"
- class="gl-m-3"
- :placeholder="$options.translations.searchMilestones"
- @input="onSearchBoxInput"
- @keydown.enter.prevent="onSearchBoxEnter"
- />
-
- <gl-dropdown-item
- :is-checked="selectedMilestones.length === 0"
- is-check-item
- @click="selectNoMilestone()"
- >
- {{ $options.translations.noMilestone }}
- </gl-dropdown-item>
-
- <gl-dropdown-divider />
-
- <template v-if="isLoading">
- <gl-loading-icon size="sm" />
- <gl-dropdown-divider />
+ <gl-collapsible-listbox
+ :header-text="$options.translations.selectMilestone"
+ :items="allMilestones"
+ :reset-button-label="$options.translations.unselect"
+ :searching="isLoading"
+ :selected="selectedMilestones"
+ :toggle-text="selectedMilestonesLabel"
+ block
+ multiple
+ searchable
+ @reset="selectNoMilestone"
+ @search="onSearchBoxInput"
+ @select="selectMilestone"
+ >
+ <template #group-label="{ group }">
+ <span :data-testid="`${group.id}-section`"
+ >{{ group.text }}<gl-badge size="sm" class="gl-ml-2">{{ group.totalCount }}</gl-badge></span
+ >
</template>
- <template v-else-if="showNoResults">
- <div class="dropdown-item-space">
- <span data-testid="milestone-combobox-no-results" class="gl-pl-6">{{
- $options.translations.noResultsLabel
- }}</span>
+ <template #footer>
+ <div
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-display-flex gl-flex-direction-column gl-p-2! gl-pt-0!"
+ >
+ <gl-button
+ v-for="(item, idx) in extraLinks"
+ :key="idx"
+ :href="item.url"
+ is-check-item
+ data-testid="milestone-combobox-extra-links"
+ category="tertiary"
+ block
+ class="gl-justify-content-start! gl-mt-2!"
+ >
+ {{ item.text }}
+ </gl-button>
</div>
- <gl-dropdown-divider />
- </template>
- <template v-else>
- <milestone-results-section
- v-if="showProjectMilestoneSection"
- :section-title="$options.translations.projectMilestones"
- :total-count="matches.projectMilestones.totalCount"
- :items="matches.projectMilestones.list"
- :selected-milestones="selectedMilestones"
- :error="matches.projectMilestones.error"
- :error-message="$options.translations.searchErrorMessage"
- data-testid="project-milestones-section"
- @selected="selectMilestone($event)"
- />
-
- <milestone-results-section
- v-if="showGroupMilestoneSection"
- :section-title="$options.translations.groupMilestones"
- :total-count="matches.groupMilestones.totalCount"
- :items="matches.groupMilestones.list"
- :selected-milestones="selectedMilestones"
- :error="matches.groupMilestones.error"
- :error-message="$options.translations.searchErrorMessage"
- data-testid="group-milestones-section"
- @selected="selectMilestone($event)"
- />
</template>
- <gl-dropdown-item
- v-for="(item, idx) in extraLinks"
- :key="idx"
- :href="item.url"
- is-check-item
- data-testid="milestone-combobox-extra-links"
- >
- {{ item.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js
index 1f88c0a1ea6..c0cd58cc5d2 100644
--- a/app/assets/javascripts/milestones/stores/mutations.js
+++ b/app/assets/javascripts/milestones/stores/mutations.js
@@ -37,7 +37,7 @@ export default {
},
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = {
- list: response.data.map(({ title }) => ({ title })),
+ list: response.data.map(({ title }) => ({ text: title, value: title })),
totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
@@ -51,7 +51,7 @@ export default {
},
[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) {
state.matches.groupMilestones = {
- list: response.data.map(({ title }) => ({ title })),
+ list: response.data.map(({ title }) => ({ text: title, value: title })),
totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
index 3026bce0972..c94e7648d1d 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/index/constants.js
@@ -2,9 +2,9 @@ import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export const CREATE_EXPERIMENT_HELP_PATH = helpPagePath(
- 'user/project/ml/experiment_tracking/index.md',
+ 'user/project/ml/experiment_tracking/index',
{
- anchor: 'tracking-new-experiments-and-trials',
+ anchor: 'track-new-experiments-and-candidates',
},
);
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
index 4d34555ac2f..346c2453715 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/constants.js
@@ -1,5 +1,4 @@
import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
export const METRIC_KEY_PREFIX = 'metric.';
export const LIST_KEY_CREATED_AT = 'created_at';
@@ -13,9 +12,3 @@ export const BASE_SORT_FIELDS = Object.freeze([
label: s__('MlExperimentTracking|Created at'),
},
]);
-export const CREATE_CANDIDATE_HELP_PATH = helpPagePath(
- 'user/project/ml/experiment_tracking/index.md',
- {
- anchor: 'tracking-new-experiments-and-trials',
- },
-);
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 28a27059b17..afd48df93e4 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
@@ -10,12 +10,8 @@ import KeysetPagination from '~/vue_shared/components/incubation/pagination.vue'
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
-import {
- LIST_KEY_CREATED_AT,
- BASE_SORT_FIELDS,
- METRIC_KEY_PREFIX,
- CREATE_CANDIDATE_HELP_PATH,
-} from './constants';
+import { CREATE_EXPERIMENT_HELP_PATH as CREATE_CANDIDATE_HELP_PATH } from '../index/constants';
+import { LIST_KEY_CREATED_AT, BASE_SORT_FIELDS, METRIC_KEY_PREFIX } from './constants';
import * as translations from './translations';
export default {
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
index f9e5f82e708..92d159f68be 100644
--- a/app/assets/javascripts/ml/model_registry/apps/index.js
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -1,3 +1,5 @@
import ShowMlModel from './show_ml_model.vue';
+import ShowMlModelVersion from './show_ml_model_version.vue';
+import IndexMlModels from './index_ml_models.vue';
-export { ShowMlModel };
+export { ShowMlModel, ShowMlModelVersion, IndexMlModels };
diff --git a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
new file mode 100644
index 00000000000..5a55d5669a8
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
@@ -0,0 +1,61 @@
+<script>
+import { isEmpty } from 'lodash';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import * as i18n from '../translations';
+import { BASE_SORT_FIELDS } from '../constants';
+import SearchBar from '../components/search_bar.vue';
+import ModelRow from '../components/model_row.vue';
+
+export default {
+ name: 'IndexMlModels',
+ components: {
+ Pagination,
+ ModelRow,
+ SearchBar,
+ MetadataItem,
+ TitleArea,
+ },
+ props: {
+ models: {
+ type: Array,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ modelCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ hasModels() {
+ return !isEmpty(this.models);
+ },
+ },
+ i18n,
+ sortableFields: BASE_SORT_FIELDS,
+};
+</script>
+
+<template>
+ <div>
+ <title-area :title="$options.i18n.TITLE_LABEL">
+ <template #metadata-models-count>
+ <metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(modelCount)" />
+ </template>
+ </title-area>
+
+ <template v-if="hasModels">
+ <search-bar :sortable-fields="$options.sortableFields" />
+ <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/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
index d4f17c840d7..e8ec8f157ef 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -1,16 +1,71 @@
<script>
+import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import * as i18n from '../translations';
+
export default {
name: 'ShowMlModelApp',
- components: {},
+ components: {
+ TitleArea,
+ GlTabs,
+ GlTab,
+ GlBadge,
+ MetadataItem,
+ },
props: {
model: {
type: Object,
required: true,
},
},
+ computed: {
+ versionCount() {
+ return this.model.versionCount || 0;
+ },
+ candidateCount() {
+ return this.model.candidateCount || 0;
+ },
+ },
+ i18n,
};
</script>
<template>
- <div>{{ model.name }}</div>
+ <div>
+ <title-area :title="model.name">
+ <template #metadata-versions-count>
+ <metadata-item
+ icon="machine-learning"
+ :text="$options.i18n.versionsCountLabel(model.versionCount)"
+ />
+ </template>
+
+ <template #sub-header>
+ {{ model.description }}
+ </template>
+ </title-area>
+
+ <gl-tabs class="gl-mt-4">
+ <gl-tab :title="$options.i18n.MODEL_DETAILS_TAB_LABEL">
+ <h3 class="gl-font-lg">{{ $options.i18n.LATEST_VERSION_LABEL }}</h3>
+ <template v-if="model.latestVersion">
+ {{ model.latestVersion.version }}
+ </template>
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_VERSIONS_LABEL }}</div>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.MODEL_OTHER_VERSIONS_TAB_LABEL }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ versionCount }}</gl-badge>
+ </template>
+ </gl-tab>
+ <gl-tab>
+ <template #title>
+ {{ $options.i18n.MODEL_CANDIDATES_TAB_LABEL }}
+ <gl-badge size="sm" class="gl-tab-counter-badge">{{ candidateCount }}</gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ </div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
new file mode 100644
index 00000000000..a9440aff1ce
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ name: 'ShowMlModelVersionApp',
+ components: {},
+ props: {
+ modelVersion: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>{{ modelVersion.model.name }} - {{ modelVersion.version }}</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/components/model_row.vue
index 4f91f0939a8..ffae7e83099 100644
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/model_row.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_row.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { modelVersionCountMessage } from '../translations';
+import { s__, n__ } from '~/locale';
export default {
name: 'MlModelRow',
@@ -17,8 +17,16 @@ export default {
hasVersions() {
return this.model.version != null;
},
+ modelVersionCountMessage() {
+ if (!this.model.versionCount) return s__('MlModelRegistry|No registered versions');
+
+ return n__(
+ 'MlModelRegistry|· No other versions',
+ 'MlModelRegistry|· %d versions',
+ this.model.versionCount,
+ );
+ },
},
- modelVersionCountMessage,
};
</script>
@@ -29,7 +37,9 @@ export default {
</gl-link>
<div class="gl-text-secondary">
- {{ $options.modelVersionCountMessage(model.version, model.versionCount) }}
+ <gl-link v-if="hasVersions" :href="model.versionPath">{{ model.version }}</gl-link>
+
+ {{ modelVersionCountMessage }}
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/search_bar.vue b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
new file mode 100644
index 00000000000..2bcdabc403f
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/search_bar.vue
@@ -0,0 +1,71 @@
+<script>
+import { queryToObject, setUrlParams, visitUrl } from '~/lib/utils/url_utility';
+import { LIST_KEY_CREATED_AT } from '~/ml/experiment_tracking/routes/experiments/show/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+
+export default {
+ name: 'SearchBar',
+ components: {
+ RegistrySearch,
+ },
+ props: {
+ sortableFields: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ const query = queryToObject(window.location.search);
+
+ const filter = query.name ? [{ value: { data: query.name }, type: FILTERED_SEARCH_TERM }] : [];
+
+ const orderBy = query.orderBy || LIST_KEY_CREATED_AT;
+
+ return {
+ filters: filter,
+ sorting: {
+ orderBy,
+ sort: (query.sort || 'desc').toLowerCase(),
+ },
+ };
+ },
+ methods: {
+ submitFilters() {
+ return visitUrl(setUrlParams(this.parsedQuery()));
+ },
+ parsedQuery() {
+ const name = this.filters
+ .map((f) => f.value.data)
+ .join(' ')
+ .trim();
+
+ const filterByQuery = name === '' ? {} : { name };
+
+ return { ...filterByQuery, ...this.sorting };
+ },
+ updateFilters(newValue) {
+ this.filters = newValue;
+ },
+ updateSorting(newValue) {
+ this.sorting = { ...this.sorting, ...newValue };
+ },
+ updateSortingAndEmitUpdate(newValue) {
+ this.updateSorting(newValue);
+ this.submitFilters();
+ },
+ },
+};
+</script>
+
+<template>
+ <registry-search
+ :filters="filters"
+ :sorting="sorting"
+ :sortable-fields="sortableFields"
+ @sorting:changed="updateSortingAndEmitUpdate"
+ @filter:changed="updateFilters"
+ @filter:submit="submitFilters"
+ @filter:clear="filters = []"
+ />
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/constants.js b/app/assets/javascripts/ml/model_registry/constants.js
new file mode 100644
index 00000000000..10c21ec4f12
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/constants.js
@@ -0,0 +1,13 @@
+import { s__ } from '~/locale';
+
+export const LIST_KEY_CREATED_AT = 'created_at';
+export const BASE_SORT_FIELDS = Object.freeze([
+ {
+ orderBy: 'name',
+ label: s__('MlExperimentTracking|Name'),
+ },
+ {
+ orderBy: LIST_KEY_CREATED_AT,
+ label: s__('MlExperimentTracking|Created at'),
+ },
+]);
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
deleted file mode 100644
index 3770b4ec3ac..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/components/ml_models_index.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-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: 'MlModelRegistryApp',
- components: {
- Pagination,
- ModelRow,
- },
- props: {
- models: {
- type: Array,
- required: true,
- },
- pageInfo: {
- type: Object,
- required: true,
- },
- },
- computed: {
- hasModels() {
- return !isEmpty(this.models);
- },
- },
- i18n: translations,
-};
-</script>
-
-<template>
- <div>
- <div class="detail-page-header gl-flex-wrap">
- <div class="detail-page-header-body">
- <div class="page-title gl-flex-grow-1 gl-display-flex gl-align-items-center">
- <h2 class="gl-font-size-h-display gl-my-0">{{ $options.i18n.TITLE_LABEL }}</h2>
- </div>
- </div>
- </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/index.js b/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
deleted file mode 100644
index d303d9716af..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import MlModelsIndex from './components/ml_models_index.vue';
-
-export default MlModelsIndex;
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
deleted file mode 100644
index 9210d816373..00000000000
--- a/app/assets/javascripts/ml/model_registry/routes/models/index/translations.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { s__, n__, sprintf } from '~/locale';
-
-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/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js
new file mode 100644
index 00000000000..89b3f45ed94
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/translations.js
@@ -0,0 +1,16 @@
+import { s__, n__ } from '~/locale';
+
+export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
+export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
+export const MODEL_CANDIDATES_TAB_LABEL = s__('MlModelRegistry|Version candidates');
+export const LATEST_VERSION_LABEL = s__('MlModelRegistry|Latest version');
+export const NO_VERSIONS_LABEL = s__('MlModelRegistry|This model has no versions');
+
+export const versionsCountLabel = (versionCount) =>
+ n__('MlModelRegistry|%d version', 'MlModelRegistry|%d versions', versionCount);
+
+export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
+export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
+
+export const modelsCountLabel = (modelCount) =>
+ n__('MlModelRegistry|%d model', 'MlModelRegistry|%d models', modelCount);
diff --git a/app/assets/javascripts/mr_notes/init.js b/app/assets/javascripts/mr_notes/init.js
index 28f294589ae..5594e71641b 100644
--- a/app/assets/javascripts/mr_notes/init.js
+++ b/app/assets/javascripts/mr_notes/init.js
@@ -40,6 +40,7 @@ function setupMrNotesState(store, notesDataset, diffsDataset) {
mrReviews: getReviewsForMergeRequest(mrPath),
diffViewType:
getParameterValues('view')[0] || getCookie(DIFF_VIEW_COOKIE_NAME) || INLINE_DIFF_VIEW_TYPE,
+ perPage: Number(diffsDataset.perPage),
});
}
diff --git a/app/assets/javascripts/notes/components/comment_field_layout.vue b/app/assets/javascripts/notes/components/comment_field_layout.vue
index cefcc1b0c98..7673bd61631 100644
--- a/app/assets/javascripts/notes/components/comment_field_layout.vue
+++ b/app/assets/javascripts/notes/components/comment_field_layout.vue
@@ -1,5 +1,4 @@
<script>
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
import EmailParticipantsWarning from './email_participants_warning.vue';
import AttachmentsWarning from './attachments_warning.vue';
@@ -12,7 +11,6 @@ export default {
EmailParticipantsWarning,
NoteableWarning,
},
- mixins: [glFeatureFlagsMixin()],
props: {
noteableData: {
type: Object,
@@ -56,11 +54,7 @@ export default {
return this.emailParticipants.length && !this.isInternalNote;
},
showAttachmentWarning() {
- return (
- this.glFeatures.serviceDeskNewNoteEmailNativeAttachments &&
- this.showEmailParticipantsWarning &&
- this.containsLink
- );
+ return this.showEmailParticipantsWarning && this.containsLink;
},
},
};
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index bcf9b4cf893..a999b633f64 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -24,7 +24,9 @@ export default {
},
lockedIssueWarning() {
return sprintf(
- __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ __(
+ 'The discussion in this %{issuableDisplayName} is locked. Only project members can comment.',
+ ),
{ issuableDisplayName: this.issuableDisplayName },
);
},
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
index b1aee19d5b2..cc4f360a694 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue
@@ -21,7 +21,11 @@ export default {
</script>
<template>
- <gl-button :loading="isResolving" class="gl-xs-w-full ml-sm-2" @click="$emit('onClick')">
+ <gl-button
+ :loading="isResolving"
+ class="gl-w-full gl-sm-w-auto ml-sm-2"
+ @click="$emit('onClick')"
+ >
{{ buttonTitle }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index 4ccba011014..34cbba8ce43 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -29,7 +29,7 @@ export default {
:href="url"
:title="$options.i18n.buttonLabel"
:aria-label="$options.i18n.buttonLabel"
- class="new-issue-for-discussion discussion-create-issue-btn gl-xs-w-full"
+ class="new-issue-for-discussion discussion-create-issue-btn gl-w-full gl-sm-w-auto"
icon="issue-new"
/>
</div>
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 30d3bfcb989..738af4f6064 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -35,5 +35,5 @@ export default {
</script>
<template>
- <div v-safe-html="signedOutText" class="disabled-comment text-center"></div>
+ <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-text-secondary"></div>
</template>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index e0b1f7a8c6a..493beb8cea9 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -290,9 +290,6 @@ export default {
parent: this.$el,
});
},
- deleteNoteHandler(note) {
- this.$emit('noteDeleted', this.discussion, note);
- },
onStartReplying(discussionId) {
if (this.discussion.id === discussionId) {
this.showReplyForm();
@@ -329,7 +326,6 @@ export default {
:is-overview-tab="isOverviewTab"
:should-scroll-to-note="shouldScrollToNote"
@startReplying="showReplyForm"
- @deleteNote="deleteNoteHandler"
>
<template #avatar-badge>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 809b1716b91..c817655b649 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -105,6 +105,11 @@ export default {
required: false,
default: true,
},
+ discussion: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
data() {
return {
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index ce642733396..b4eeea8db02 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -40,7 +40,7 @@ export default {
showAiActions() {
return (
this.resourceGlobalId &&
- this.glFeatures.openaiExperimentation &&
+ (this.glFeatures.openaiExperimentation || this.glFeatures.aiGlobalSwitch) &&
this.glFeatures.summarizeNotes
);
},
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 966f4184780..a995b9fa214 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -318,11 +318,6 @@ export default {
const note = noteData;
const selectedDiscussion = state.discussions.find((disc) => disc.id === note.id);
note.expanded = true; // override expand flag to prevent collapse
- if (note.diff_file) {
- Object.assign(note, {
- file_hash: note.diff_file.file_hash,
- });
- }
Object.assign(selectedDiscussion, { ...note });
},
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 2e976cd6230..32ff7fff128 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,12 +1,15 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
+import { logError } from '~/lib/logger';
+import { DEFAULT_SORTING_OPTION, SORTING_OPTIONS } from './constants';
function reportErrorAndThrow(e) {
+ logError(e);
Sentry.captureException(e);
throw e;
}
// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L59
-async function enableTraces(provisioningUrl) {
+async function enableObservability(provisioningUrl) {
try {
// Note: axios.put(url, undefined, {withCredentials: true}) does not send cookies properly, so need to use the API below for the correct behaviour
return await axios(provisioningUrl, {
@@ -19,7 +22,7 @@ async function enableTraces(provisioningUrl) {
}
// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L37
-async function isTracingEnabled(provisioningUrl) {
+async function isObservabilityEnabled(provisioningUrl) {
try {
const { data } = await axios.get(provisioningUrl, { withCredentials: true });
if (data && data.status) {
@@ -42,18 +45,11 @@ async function fetchTrace(tracingUrl, traceId) {
throw new Error('traceId is required.');
}
- const { data } = await axios.get(tracingUrl, {
+ const { data } = await axios.get(`${tracingUrl}/${traceId}`, {
withCredentials: true,
- params: {
- trace_id: traceId,
- },
});
- if (!Array.isArray(data.traces) || data.traces.length === 0) {
- throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
- }
-
- return data.traces[0];
+ return data;
} catch (e) {
return reportErrorAndThrow(e);
}
@@ -65,9 +61,10 @@ async function fetchTrace(tracingUrl, traceId) {
const SUPPORTED_FILTERS = {
durationMs: ['>', '<'],
operation: ['=', '!='],
- serviceName: ['=', '!='],
+ service: ['=', '!='],
period: ['='],
traceId: ['=', '!='],
+ attribute: ['='],
// free-text 'search' temporarily ignored https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2309
};
@@ -77,9 +74,10 @@ const SUPPORTED_FILTERS = {
const FILTER_TO_QUERY_PARAM = {
durationMs: 'duration_nano',
operation: 'operation',
- serviceName: 'service_name',
+ service: 'service_name',
period: 'period',
traceId: 'trace_id',
+ attribute: 'attribute',
};
const FILTER_OPERATORS_PREFIX = {
@@ -112,13 +110,32 @@ function getFilterParamName(filterName, operator) {
}
/**
+ * Process `filterValue` and append the proper query params to the `searchParams` arg
+ *
+ * It mutates `searchParams`
+ *
+ * @param {String} filterValue The filter value, in the format `attribute_name=attribute_value`
+ * @param {String} filterOperator The filter operator
+ * @param {URLSearchParams} searchParams The URLSearchParams object where to append the proper query params
+ */
+function handleAttributeFilter(filterValue, filterOperator, searchParams) {
+ const [attrName, attrValue] = filterValue.split('=');
+ if (attrName && attrValue) {
+ if (filterOperator === '=') {
+ searchParams.append('attr_name', attrName);
+ searchParams.append('attr_value', attrValue);
+ }
+ }
+}
+
+/**
* Builds URLSearchParams from a filter object of type { [filterName]: undefined | null | Array<{operator: String, value: any} }
* e.g:
*
* filterObj = {
* durationMs: [{operator: '>', value: '100'}, {operator: '<', value: '1000' }],
* operation: [{operator: '=', value: 'someOp' }],
- * serviceName: [{operator: '!=', value: 'foo' }]
+ * service: [{operator: '!=', value: 'foo' }]
* }
*
* It handles converting the filter to the proper supported query params
@@ -131,20 +148,22 @@ function filterObjToQueryParams(filterObj) {
Object.keys(SUPPORTED_FILTERS).forEach((filterName) => {
const filterValues = filterObj[filterName] || [];
- const supportedFilters = filterValues.filter((f) =>
+ const validFilters = filterValues.filter((f) =>
SUPPORTED_FILTERS[filterName].includes(f.operator),
);
- supportedFilters.forEach(({ operator, value: rawValue }) => {
- const paramName = getFilterParamName(filterName, operator);
-
- let value = rawValue;
- if (filterName === 'durationMs') {
- // converting durationMs to duration_nano
- value *= 1000000;
- }
-
- if (paramName && value) {
- filterParams.append(paramName, value);
+ validFilters.forEach(({ operator, value: rawValue }) => {
+ if (filterName === 'attribute') {
+ handleAttributeFilter(rawValue, operator, filterParams);
+ } else {
+ const paramName = getFilterParamName(filterName, operator);
+ let value = rawValue;
+ if (filterName === 'durationMs') {
+ // converting durationMs to duration_nano
+ value *= 1000000;
+ }
+ if (paramName && value) {
+ filterParams.append(paramName, value);
+ }
}
});
});
@@ -161,12 +180,12 @@ function filterObjToQueryParams(filterObj) {
* {
* durationMs: [ {operator: '>', value: '100'}, {operator: '<', value: '1000'}],
* operation: [ {operator: '=', value: 'someOp}],
- * serviceName: [ {operator: '!=', value: 'foo}]
+ * service: [ {operator: '!=', value: 'foo}]
* }
*
* @returns Array<Trace> : A list of traces
*/
-async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {}) {
+async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize, sortBy } = {}) {
const params = filterObjToQueryParams(filters);
if (pageToken) {
params.append('page_token', pageToken);
@@ -174,6 +193,10 @@ async function fetchTraces(tracingUrl, { filters = {}, pageToken, pageSize } = {
if (pageSize) {
params.append('page_size', pageSize);
}
+ const sortOrder = Object.values(SORTING_OPTIONS).includes(sortBy)
+ ? sortBy
+ : DEFAULT_SORTING_OPTION;
+ params.append('sort', sortOrder);
try {
const { data } = await axios.get(tracingUrl, {
@@ -228,18 +251,54 @@ async function fetchOperations(operationsUrl, serviceName) {
}
}
-export function buildClient({ provisioningUrl, tracingUrl, servicesUrl, operationsUrl } = {}) {
- if (!provisioningUrl || !tracingUrl || !servicesUrl || !operationsUrl) {
- throw new Error(
- 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
- );
+async function fetchMetrics(metricsUrl) {
+ try {
+ const { data } = await axios.get(metricsUrl, {
+ withCredentials: true,
+ });
+ if (!Array.isArray(data.metrics)) {
+ throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+ return data;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
+export function buildClient(config) {
+ if (!config) {
+ throw new Error('No options object provided'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+
+ const { provisioningUrl, tracingUrl, servicesUrl, operationsUrl, metricsUrl } = config;
+
+ if (typeof provisioningUrl !== 'string') {
+ throw new Error('provisioningUrl param must be a string');
}
+
+ if (typeof tracingUrl !== 'string') {
+ throw new Error('tracingUrl param must be a string');
+ }
+
+ if (typeof servicesUrl !== 'string') {
+ throw new Error('servicesUrl param must be a string');
+ }
+
+ if (typeof operationsUrl !== 'string') {
+ throw new Error('operationsUrl param must be a string');
+ }
+
+ if (typeof metricsUrl !== 'string') {
+ throw new Error('metricsUrl param must be a string');
+ }
+
return {
- enableTraces: () => enableTraces(provisioningUrl),
- isTracingEnabled: () => isTracingEnabled(provisioningUrl),
- fetchTraces: (filters) => fetchTraces(tracingUrl, filters),
+ enableObservability: () => enableObservability(provisioningUrl),
+ isObservabilityEnabled: () => isObservabilityEnabled(provisioningUrl),
+ fetchTraces: (options) => fetchTraces(tracingUrl, options),
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
+ fetchMetrics: () => fetchMetrics(metricsUrl),
};
}
diff --git a/app/assets/javascripts/observability/components/loader/constants.js b/app/assets/javascripts/observability/components/loader/constants.js
new file mode 100644
index 00000000000..5c2d8ad0d1b
--- /dev/null
+++ b/app/assets/javascripts/observability/components/loader/constants.js
@@ -0,0 +1,20 @@
+import { __ } from '~/locale';
+
+export const CONTENT_STATE = Object.freeze({
+ ERROR: 'error',
+ LOADED: 'loaded',
+});
+
+export const LOADER_STATE = Object.freeze({
+ ERROR: 'error',
+ VISIBLE: 'visible',
+ HIDDEN: 'hidden',
+});
+
+export const DEFAULT_TIMERS = Object.freeze({
+ TIMEOUT_MS: 20000,
+ CONTENT_WAIT_MS: 500,
+});
+
+export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
+export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/loader/index.vue
index c3d0a7c90b1..6b92dc428d2 100644
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ b/app/assets/javascripts/observability/components/loader/index.vue
@@ -1,31 +1,30 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import {
- SKELETON_STATE,
+ LOADER_STATE,
+ CONTENT_STATE,
DEFAULT_TIMERS,
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
- SKELETON_SPINNER_VARIANT,
-} from '../../constants';
+} from './constants';
export default {
components: {
- GlSkeletonLoader,
GlAlert,
GlLoadingIcon,
},
- SKELETON_STATE,
+ LOADER_STATE,
i18n: {
TIMEOUT_ERROR_LABEL,
TIMEOUT_ERROR_MESSAGE,
},
props: {
- variant: {
+ contentState: {
type: String,
required: false,
- default: '',
+ default: null,
},
},
data() {
@@ -35,18 +34,25 @@ export default {
errorTimeout: null,
};
},
+
computed: {
- skeletonVisible() {
- return this.state === SKELETON_STATE.VISIBLE;
+ loaderVisible() {
+ return this.state === LOADER_STATE.VISIBLE;
},
- skeletonHidden() {
- return this.state === SKELETON_STATE.HIDDEN;
+ loaderHidden() {
+ return this.state === LOADER_STATE.HIDDEN;
},
errorVisible() {
- return this.state === SKELETON_STATE.ERROR;
+ return this.state === LOADER_STATE.ERROR;
},
- spinnerVariant() {
- return this.variant === SKELETON_SPINNER_VARIANT;
+ },
+ watch: {
+ contentState(newValue) {
+ if (newValue === CONTENT_STATE.LOADED) {
+ this.onContentLoaded();
+ } else if (newValue === CONTENT_STATE.ERROR) {
+ this.onError();
+ }
},
},
mounted() {
@@ -62,7 +68,7 @@ export default {
clearTimeout(this.errorTimeout);
clearTimeout(this.loadingTimeout);
- this.hideSkeleton();
+ this.hideLoader();
},
onError() {
clearTimeout(this.errorTimeout);
@@ -74,10 +80,10 @@ export default {
this.loadingTimeout = setTimeout(() => {
/**
* If content is not loaded within CONTENT_WAIT_MS,
- * show the skeleton
+ * show the loader
*/
- if (this.state !== SKELETON_STATE.HIDDEN) {
- this.showSkeleton();
+ if (this.state !== LOADER_STATE.HIDDEN) {
+ this.showLoader();
}
}, DEFAULT_TIMERS.CONTENT_WAIT_MS);
},
@@ -87,19 +93,19 @@ export default {
* If content is not loaded within TIMEOUT_MS,
* show the error dialog
*/
- if (this.state !== SKELETON_STATE.HIDDEN) {
+ if (this.state !== LOADER_STATE.HIDDEN) {
this.showError();
}
}, DEFAULT_TIMERS.TIMEOUT_MS);
},
- hideSkeleton() {
- this.state = SKELETON_STATE.HIDDEN;
+ hideLoader() {
+ this.state = LOADER_STATE.HIDDEN;
},
- showSkeleton() {
- this.state = SKELETON_STATE.VISIBLE;
+ showLoader() {
+ this.state = LOADER_STATE.VISIBLE;
},
showError() {
- this.state = SKELETON_STATE.ERROR;
+ this.state = LOADER_STATE.ERROR;
},
},
};
@@ -107,19 +113,12 @@ export default {
<template>
<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">
- <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" />
- <rect y="2" x="35" width="15" height="8" />
- <rect y="15" width="400" height="30" />
- </gl-skeleton-loader>
+ <div v-if="loaderVisible" class="gl-px-5 gl-my-5">
+ <gl-loading-icon size="lg" />
</div>
- <!-- The double condition is only here temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
<div
- v-else-if="spinnerVariant && skeletonHidden"
+ v-else-if="loaderHidden"
data-testid="content-wrapper"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
>
@@ -136,16 +135,5 @@ export default {
>
{{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
</gl-alert>
-
- <!-- This is only kept temporarily for back-compatibility reasons. Will be removed in next iteration https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2275 -->
- <transition v-if="!spinnerVariant">
- <div
- v-show="skeletonHidden"
- data-testid="content-wrapper"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
- >
- <slot></slot>
- </div>
- </transition>
</div>
</template>
diff --git a/app/assets/javascripts/observability/components/observability_container.vue b/app/assets/javascripts/observability/components/observability_container.vue
index 1518c132560..b89c2624f81 100644
--- a/app/assets/javascripts/observability/components/observability_container.vue
+++ b/app/assets/javascripts/observability/components/observability_container.vue
@@ -1,32 +1,17 @@
<script>
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { logError } from '~/lib/logger';
import { buildClient } from '../client';
-import { SKELETON_SPINNER_VARIANT } from '../constants';
-import ObservabilitySkeleton from './skeleton/index.vue';
+import ObservabilityLoader from './loader/index.vue';
+import { CONTENT_STATE } from './loader/constants';
export default {
- SKELETON_SPINNER_VARIANT,
components: {
- ObservabilitySkeleton,
+ ObservabilityLoader,
},
props: {
- oauthUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- servicesUrl: {
- type: String,
- required: true,
- },
- operationsUrl: {
- type: String,
+ apiConfig: {
+ type: Object,
required: true,
},
},
@@ -34,6 +19,7 @@ export default {
return {
observabilityClient: null,
authCompleted: false,
+ loaderContentState: null,
};
},
mounted() {
@@ -53,7 +39,7 @@ export default {
},
methods: {
messageHandler(e) {
- const isExpectedOrigin = e.origin === new URL(this.oauthUrl).origin;
+ const isExpectedOrigin = e.origin === new URL(this.apiConfig.oauthUrl).origin;
if (!isExpectedOrigin) return;
const { data } = e;
@@ -63,17 +49,14 @@ export default {
const { status, message, statusCode } = data;
if (status === 'success') {
- this.observabilityClient = buildClient({
- provisioningUrl: this.provisioningUrl,
- tracingUrl: this.tracingUrl,
- servicesUrl: this.servicesUrl,
- operationsUrl: this.operationsUrl,
- });
- this.$refs.observabilitySkeleton?.onContentLoaded();
+ this.observabilityClient = buildClient(this.apiConfig);
+ this.$emit('observability-client-ready', this.observabilityClient);
+ this.loaderContentState = CONTENT_STATE.LOADED;
} else if (status === 'error') {
- // eslint-disable-next-line @gitlab/require-i18n-strings,no-console
- console.error('GOB auth failed with error:', message, statusCode);
- this.$refs.observabilitySkeleton?.onError();
+ const error = new Error(`GOB auth failed with error: ${message} - status: ${statusCode}`);
+ Sentry.captureException(error);
+ logError(error);
+ this.loaderContentState = CONTENT_STATE.ERROR;
}
this.authCompleted = true;
}
@@ -88,15 +71,12 @@ export default {
v-if="!authCompleted"
sandbox="allow-same-origin allow-forms allow-scripts"
hidden
- :src="oauthUrl"
+ :src="apiConfig.oauthUrl"
data-testid="observability-oauth-iframe"
></iframe>
- <observability-skeleton
- ref="observabilitySkeleton"
- :variant="$options.SKELETON_SPINNER_VARIANT"
- >
+ <observability-loader :content-state="loaderContentState">
<slot v-if="observabilityClient" :observability-client="observabilityClient"></slot>
- </observability-skeleton>
+ </observability-loader>
</div>
</template>
diff --git a/app/assets/javascripts/observability/components/observability_empty_state.vue b/app/assets/javascripts/observability/components/observability_empty_state.vue
new file mode 100644
index 00000000000..d4d8b887934
--- /dev/null
+++ b/app/assets/javascripts/observability/components/observability_empty_state.vue
@@ -0,0 +1,36 @@
+<script>
+import EMPTY_TRACING_SVG from '@gitlab/svgs/dist/illustrations/monitoring/tracing.svg?url';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ EMPTY_TRACING_SVG,
+ i18n: {
+ title: s__('Observability|Get started with GitLab Observability'),
+ description: s__('Observability|Monitor your applications with GitLab Observability.'),
+ enableButtonText: s__('Observability|Enable'),
+ },
+ components: {
+ GlEmptyState,
+ GlButton,
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="$options.EMPTY_TRACING_SVG"
+ :svg-height="null"
+ >
+ <template #description>
+ <span>{{ $options.i18n.description }}</span>
+ </template>
+
+ <template #actions>
+ <gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="$emit('enable-observability')">
+ {{ $options.i18n.enableButtonText }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/observability/components/provisioned_observability_container.vue b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
new file mode 100644
index 00000000000..95ffd54fd1d
--- /dev/null
+++ b/app/assets/javascripts/observability/components/provisioned_observability_container.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import ObservabilityEmptyState from './observability_empty_state.vue';
+
+export default {
+ components: {
+ ObservabilityContainer,
+ ObservabilityEmptyState,
+ GlLoadingIcon,
+ },
+ props: {
+ apiConfig: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ /**
+ * observabilityEnabled: boolean | null.
+ * null identifies a state where we don't know if observability is enabled or not (e.g. when fetching the status from the API fails)
+ */
+ observabilityEnabled: null,
+ observabilityClient: null,
+ };
+ },
+ computed: {
+ isObservabilityStatusKnown() {
+ return this.observabilityEnabled !== null;
+ },
+ isObservabilityDisabled() {
+ return this.observabilityEnabled === false;
+ },
+ isObservabilityEnabled() {
+ return this.observabilityEnabled;
+ },
+ },
+ methods: {
+ onObservabilityClientReady(client) {
+ this.observabilityClient = client;
+ this.checkEnabled();
+ },
+ async checkEnabled() {
+ this.loading = true;
+ try {
+ this.observabilityEnabled = await this.observabilityClient.isObservabilityEnabled();
+ } catch (e) {
+ createAlert({
+ message: s__('Observability|Error: Failed to load page. Try reloading the page.'),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ async onEnableObservability() {
+ this.loading = true;
+ try {
+ await this.observabilityClient.enableObservability();
+ this.observabilityEnabled = true;
+ } catch (e) {
+ createAlert({
+ message: s__(
+ 'Observability|Error: Failed to enable GitLab Observability. Please retry later.',
+ ),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <observability-container
+ :api-config="apiConfig"
+ @observability-client-ready="onObservabilityClientReady"
+ >
+ <div v-if="loading" class="gl-py-5">
+ <gl-loading-icon size="lg" />
+ </div>
+
+ <template v-else-if="isObservabilityStatusKnown">
+ <observability-empty-state
+ v-if="isObservabilityDisabled"
+ @enable-observability="onEnableObservability"
+ />
+ <slot v-if="isObservabilityEnabled" :observability-client="observabilityClient"></slot>
+ </template>
+ </observability-container>
+</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
index 83eaea185e5..34c43a10fc0 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/constants.js
@@ -1,17 +1,7 @@
-import { __ } from '~/locale';
-
-export const SKELETON_SPINNER_VARIANT = 'spinner';
-
-export const SKELETON_STATE = Object.freeze({
- ERROR: 'error',
- VISIBLE: 'visible',
- HIDDEN: 'hidden',
-});
-
-export const DEFAULT_TIMERS = Object.freeze({
- TIMEOUT_MS: 20000,
- CONTENT_WAIT_MS: 500,
-});
-
-export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
-export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
+export const SORTING_OPTIONS = {
+ TIMESTAMP_DESC: 'timestamp_desc',
+ TIMESTAMP_ASC: 'timestamp_asc',
+ DURATION_DESC: 'duration_desc',
+ DURATION_ASC: 'duration_asc',
+};
+export const DEFAULT_SORTING_OPTION = SORTING_OPTIONS.TIMESTAMP_DESC;
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index d281a0d8a1c..725b6ac1ad8 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -281,10 +281,31 @@ export const organizationGroups = {
],
};
-export const createOrganizationResponse = {
+export const organizationCreateResponse = {
+ data: {
+ organizationCreate: {
+ organization: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ webUrl: 'http://127.0.0.1:3000/-/organizations/default',
+ },
+ errors: [],
+ },
+ },
+};
+
+export const organizationCreateResponseWithErrors = {
+ data: {
+ organizationCreate: {
+ organization: null,
+ errors: ['Path is too short (minimum is 2 characters)'],
+ },
+ },
+};
+
+export const updateOrganizationResponse = {
organization: {
- name: 'Default',
- path: '/-/organizations/default',
+ id: 'gid://gitlab/Organizations/1',
+ name: 'Default updated',
},
errors: [],
};
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
index 8f71fdfe68b..f7f7b79d52b 100644
--- a/app/assets/javascripts/organizations/new/components/app.vue
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -4,12 +4,13 @@ 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 FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import organizationCreateMutation from '../graphql/mutations/organization_create.mutation.graphql';
import NewEditForm from '../../shared/components/new_edit_form.vue';
export default {
name: 'OrganizationNewApp',
- components: { NewEditForm, GlSprintf, GlLink },
+ components: { NewEditForm, GlSprintf, GlLink, FormErrorsAlert },
i18n: {
pageTitle: s__('Organization|New organization'),
pageDescription: s__(
@@ -22,6 +23,7 @@ export default {
data() {
return {
loading: false,
+ errors: [],
};
},
computed: {
@@ -35,21 +37,22 @@ export default {
try {
const {
data: {
- createOrganization: { organization, errors },
+ organizationCreate: { organization, errors },
},
} = await this.$apollo.mutate({
- mutation: createOrganizationMutation,
+ mutation: organizationCreateMutation,
variables: {
- ...formValues,
+ input: { name: formValues.name, path: formValues.path },
},
});
if (errors.length) {
- // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/417891 is complete.
+ this.errors = errors;
+
return;
}
- visitUrlWithAlerts(organization.path, [
+ visitUrlWithAlerts(organization.webUrl, [
{
id: 'organization-successfully-created',
title: this.$options.i18n.successAlertTitle,
@@ -69,6 +72,7 @@ export default {
<template>
<div class="gl-py-6">
+ <form-errors-alert v-model="errors" />
<h1 class="gl-mt-0 gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1>
<p>
<gl-sprintf :message="$options.i18n.pageDescription">
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
deleted file mode 100644
index 766c7e96d14..00000000000
--- a/app/assets/javascripts/organizations/new/graphql/mutations/create_organization.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation createOrganization($input: LocalCreateOrganizationInput!) {
- createOrganization(input: $input) @client {
- organization {
- name
- path
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql b/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql
new file mode 100644
index 00000000000..81fbfddd1e4
--- /dev/null
+++ b/app/assets/javascripts/organizations/new/graphql/mutations/organization_create.mutation.graphql
@@ -0,0 +1,9 @@
+mutation organizationCreate($input: OrganizationCreateInput!) {
+ organizationCreate(input: $input) {
+ organization {
+ id
+ webUrl
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql b/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
deleted file mode 100644
index f708c4ad162..00000000000
--- a/app/assets/javascripts/organizations/new/graphql/typedefs.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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
index a65603227f6..9c7e5344800 100644
--- a/app/assets/javascripts/organizations/new/index.js
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -3,7 +3,6 @@ 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 = () => {
@@ -17,7 +16,7 @@ export const initOrganizationsNew = () => {
const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/organizations/profile/preferences/index.js b/app/assets/javascripts/organizations/profile/preferences/index.js
new file mode 100644
index 00000000000..0b0dd313cd8
--- /dev/null
+++ b/app/assets/javascripts/organizations/profile/preferences/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { s__ } from '~/locale';
+import OrganizationSelect from '~/vue_shared/components/entity_select/organization_select.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import resolvers from '../../shared/graphql/resolvers';
+
+export const initHomeOrganizationSetting = () => {
+ const el = document.getElementById('js-home-organization-setting');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { initialSelection } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'HomeOrganizationSetting',
+ apolloProvider,
+ render(createElement) {
+ return createElement(OrganizationSelect, {
+ props: {
+ block: true,
+ label: s__('Organization|Home organization'),
+ description: s__('Organization|Choose what organization you want to see by default.'),
+ inputName: 'home_organization',
+ inputId: 'home_organization',
+ initialSelection,
+ toggleClass: 'gl-form-input-xl',
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/settings/general/components/app.vue b/app/assets/javascripts/organizations/settings/general/components/app.vue
new file mode 100644
index 00000000000..134fcc17b54
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/app.vue
@@ -0,0 +1,14 @@
+<script>
+import OrganizationSettings from './organization_settings.vue';
+
+export default {
+ name: 'OrganizationSettingsGeneralApp',
+ components: { OrganizationSettings },
+};
+</script>
+
+<template>
+ <div>
+ <organization-settings />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
new file mode 100644
index 00000000000..14826825cd6
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
@@ -0,0 +1,77 @@
+<script>
+import { s__, __ } from '~/locale';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import updateOrganizationMutation from '../graphql/mutations/update_organization.mutation.graphql';
+
+export default {
+ name: 'OrganizationSettings',
+ components: { NewEditForm, SettingsBlock },
+ inject: ['organization'],
+ i18n: {
+ submitButtonText: __('Save changes'),
+ settingsBlock: {
+ title: s__('Organization|Organization settings'),
+ description: s__('Organization|Update your organization name, description, and avatar.'),
+ },
+ errorMessage: s__(
+ 'Organization|An error occurred updating your organization. Please try again.',
+ ),
+ successMessage: s__('Organization|Organization was successfully updated.'),
+ },
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ methods: {
+ async onSubmit(formValues) {
+ this.loading = true;
+ try {
+ const {
+ data: {
+ updateOrganization: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateOrganizationMutation,
+ variables: {
+ id: this.organization.id,
+ name: formValues.name,
+ },
+ });
+
+ if (errors.length) {
+ // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+ return;
+ }
+
+ createAlert({ message: this.$options.i18n.successMessage, variant: VARIANT_INFO });
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block default-expanded slide-animated>
+ <template #title>{{ $options.i18n.settingsBlock.title }}</template>
+ <template #description>{{ $options.i18n.settingsBlock.description }}</template>
+ <template #default>
+ <new-edit-form
+ :loading="loading"
+ :initial-form-values="organization"
+ :fields-to-render="$options.fieldsToRender"
+ :submit-button-text="$options.i18n.submitButtonText"
+ :show-cancel-button="false"
+ @submit="onSubmit"
+ />
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
new file mode 100644
index 00000000000..b571a523260
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateOrganization($input: LocalUpdateOrganizationInput!) {
+ updateOrganization(input: $input) @client {
+ organization {
+ id
+ name
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
new file mode 100644
index 00000000000..eb81a7b0321
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
@@ -0,0 +1,5 @@
+# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+input LocalUpdateOrganizationInput {
+ id: ID!
+ name: String
+}
diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js
new file mode 100644
index 00000000000..36303c32b94
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/index.js
@@ -0,0 +1,38 @@
+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 initOrganizationsSettingsGeneral = () => {
+ const el = document.getElementById('js-organizations-settings-general');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const { organization, organizationsPath, rootUrl } = convertObjectPropsToCamelCase(
+ JSON.parse(appData),
+ );
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationSettingsGeneralRoot',
+ apolloProvider,
+ provide: {
+ organization,
+ 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
index db33f240966..8aaa680036f 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -12,6 +12,7 @@ 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';
+import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '../constants';
export default {
name: 'NewEditForm',
@@ -25,43 +26,47 @@ export default {
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,
},
+ initialFormValues: {
+ type: Object,
+ required: false,
+ default() {
+ return {
+ [FORM_FIELD_NAME]: '',
+ [FORM_FIELD_PATH]: '',
+ };
+ },
+ },
+ fieldsToRender: {
+ type: Array,
+ required: false,
+ default() {
+ return [FORM_FIELD_NAME, FORM_FIELD_PATH];
+ },
+ },
+ submitButtonText: {
+ type: String,
+ required: false,
+ default: s__('Organization|Create organization'),
+ },
+ showCancelButton: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
- formValues: {
- name: '',
- path: '',
- },
+ formValues: this.initialFormValues,
hasPathBeenManuallySet: false,
};
},
@@ -69,10 +74,63 @@ export default {
baseUrl() {
return joinPaths(this.rootUrl, this.organizationsPath, '/');
},
+ fields() {
+ const fields = {
+ [FORM_FIELD_NAME]: {
+ label: s__('Organization|Organization name'),
+ validators: [formValidators.required(s__('Organization|Organization name is required.'))],
+ groupAttrs: {
+ class: this.fieldsToRender.includes(FORM_FIELD_ID)
+ ? 'gl-flex-grow-1 gl-md-form-input-lg'
+ : 'gl-flex-grow-1',
+ description: s__(
+ 'Organization|Must start with a letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
+ ),
+ },
+ inputAttrs: {
+ class: !this.fieldsToRender.includes(FORM_FIELD_ID) ? 'gl-md-form-input-lg' : null,
+ placeholder: s__('Organization|My organization'),
+ },
+ },
+ [FORM_FIELD_ID]: {
+ label: s__('Organization|Organization ID'),
+ groupAttrs: {
+ class: 'gl-md-form-input-lg gl-flex-grow-1',
+ },
+ inputAttrs: {
+ disabled: true,
+ },
+ },
+ [FORM_FIELD_PATH]: {
+ label: s__('Organization|Organization URL'),
+ validators: [
+ formValidators.required(s__('Organization|Organization URL is required.')),
+ formValidators.factory(
+ s__('Organization|Organization URL must be a minimum of two characters.'),
+ (val) => val.length >= 2,
+ ),
+ ],
+ groupAttrs: {
+ class: 'gl-w-full',
+ },
+ },
+ };
+
+ return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => {
+ if (!this.fieldsToRender.includes(fieldKey)) {
+ return accumulator;
+ }
+
+ return {
+ ...accumulator,
+ [fieldKey]: fieldDefinition,
+ };
+ }, {});
+ },
},
watch: {
'formValues.name': function watchName(value) {
- if (this.hasPathBeenManuallySet) {
+ if (this.hasPathBeenManuallySet || !this.fieldsToRender.includes(FORM_FIELD_PATH)) {
return;
}
@@ -93,7 +151,8 @@ export default {
<gl-form-fields
v-model="formValues"
:form-id="$options.formId"
- :fields="$options.fields"
+ :fields="fields"
+ class="gl-display-flex gl-column-gap-5 gl-flex-wrap"
@submit="$emit('submit', formValues)"
>
<template #input(path)="{ id, value, validation, input, blur }">
@@ -117,9 +176,11 @@ export default {
</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
+ submitButtonText
+ }}</gl-button>
+ <gl-button v-if="showCancelButton" :href="organizationsPath">{{
+ $options.i18n.cancel
}}</gl-button>
- <gl-button :href="organizationsPath">{{ $options.i18n.cancel }}</gl-button>
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
new file mode 100644
index 00000000000..010613bc9fd
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -0,0 +1,3 @@
+export const FORM_FIELD_NAME = 'name';
+export const FORM_FIELD_ID = 'id';
+export const FORM_FIELD_PATH = 'path';
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
new file mode 100644
index 00000000000..1d95786fcb0
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
@@ -0,0 +1,9 @@
+query getOrganization($id: ID!) {
+ organization(id: $id) @client {
+ id
+ name
+ descriptionHtml
+ avatarUrl
+ webUrl
+ }
+}
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 9f7e9b22e1d..9ed1be62352 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -2,7 +2,7 @@ import {
organizations,
organizationProjects,
organizationGroups,
- createOrganizationResponse,
+ updateOrganizationResponse,
} from '../../mock_data';
const simulateLoading = () => {
@@ -34,11 +34,11 @@ export default {
},
},
Mutation: {
- createOrganization: async () => {
+ updateOrganization: async () => {
// Simulate API loading
await simulateLoading();
- return createOrganizationResponse;
+ return updateOrganizationResponse;
},
},
};
diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue
new file mode 100644
index 00000000000..ae22bedd69a
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/components/app.vue
@@ -0,0 +1,51 @@
+<script>
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import organizationUsersQuery from '../graphql/organization_users.query.graphql';
+
+export default {
+ name: 'OrganizationsUsersApp',
+ i18n: {
+ users: __('Users'),
+ loadingPlaceholder: __('Loading'),
+ errorMessage: s__(
+ 'Organization|An error occurred loading the organization users. Please refresh the page to try again.',
+ ),
+ },
+ inject: ['organizationGid'],
+ data() {
+ return {
+ users: [],
+ };
+ },
+ apollo: {
+ users: {
+ query: organizationUsersQuery,
+ variables() {
+ return { id: this.organizationGid };
+ },
+ update(data) {
+ return data.organization.organizationUsers.nodes;
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.users.loading;
+ },
+ },
+};
+</script>
+
+<template>
+ <section>
+ <h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1>
+ <template v-if="loading">
+ {{ $options.i18n.loadingPlaceholder }}
+ </template>
+ <div data-testid="organization-users">{{ users }}</div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
new file mode 100644
index 00000000000..a0b2a639401
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
@@ -0,0 +1,17 @@
+query getOrganizationUsers($id: OrganizationsOrganizationID!) {
+ organization(id: $id) {
+ id
+ organizationUsers {
+ nodes {
+ badges {
+ text
+ variant
+ }
+ id
+ user {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/users/index.js b/app/assets/javascripts/organizations/users/index.js
new file mode 100644
index 00000000000..76656243075
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import OrganizationsUsersApp from './components/app.vue';
+
+export const initOrganizationsUsers = () => {
+ const el = document.getElementById('js-organizations-users');
+
+ if (!el) return false;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { organizationGid } = convertObjectPropsToCamelCase(el.dataset);
+
+ return new Vue({
+ el,
+ name: 'OrganizationsUsersRoot',
+ apolloProvider,
+ provide: {
+ organizationGid,
+ },
+ render(createElement) {
+ return createElement(OrganizationsUsersApp);
+ },
+ });
+};
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 7c594a6c091..934bb206cc4 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
@@ -219,7 +219,6 @@ export default {
<template>
<div>
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.sortableFields"
:default-order="$options.sortableFields[0].orderBy"
default-sort="asc"
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 a1c4d7ea1f2..89a8c4c2a2f 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
@@ -293,7 +293,6 @@ export default {
</template>
</registry-header>
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.searchConfig"
:default-order="$options.searchConfig[0].orderBy"
default-sort="desc"
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
index bfe0c250dd9..cf1ee44b82e 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue
@@ -44,12 +44,7 @@ export default {
<template>
<list-item v-bind="$attrs">
<template #left-primary>
- <router-link
- class="gl-text-body gl-font-weight-bold"
- data-testid="details-link"
- data-qa-selector="registry_image_content"
- :to="linkTo"
- >
+ <router-link class="gl-text-body gl-font-weight-bold" data-testid="details-link" :to="linkTo">
{{ item.name }}
</router-link>
<clipboard-button
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
index bff32a124bc..bf0cdd5db10 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
@@ -135,7 +135,6 @@ export default {
<div class="gl-my-3">
<details-header :images-detail="imagesDetail" />
<persisted-search
- class="gl-mb-5"
:sortable-fields="$options.searchConfig.nameSortFields"
:default-order="$options.searchConfig.nameSortFields[0].orderBy"
default-sort="asc"
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 b49c448c478..a821a2483cd 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
@@ -167,7 +167,7 @@ export default {
<gl-tabs>
<gl-tab :title="__('Detail')">
- <div data-qa-selector="package_information_content">
+ <div>
<package-history :package-entity="packageEntity" :project-name="projectName" />
<terraform-installation />
</div>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
index cd5f9f5a676..9d70391a8dd 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue
@@ -34,7 +34,7 @@ export default {
</script>
<template>
- <title-area :title="packageEntity.name" data-qa-selector="package_title">
+ <title-area :title="packageEntity.name">
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<gl-sprintf :message="$options.i18n.packageInfo">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
index a3bbd569f41..937553e25cc 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import Composer from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue';
import Conan from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue';
@@ -41,9 +41,6 @@ export default {
apollo: {
packageMetadata: {
query: getPackageMetadataQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
id: this.packageId,
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
index c8924e6548b..7b3acaf2ab6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
@@ -12,7 +12,7 @@ import {
GlSprintf,
GlKeysetPagination,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert';
import { NEXT, PREV } from '~/vue_shared/components/pagination/constants';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -92,9 +92,6 @@ export default {
apollo: {
packageFiles: {
query: getPackageFilesQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return this.queryVariables;
},
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
index 663c361819e..32f94b82fa3 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { first } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, n__ } from '~/locale';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
index cdf03d64b27..db5e007b81f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -77,7 +77,6 @@ export default {
v-gl-resize-observer="checkBreakpoints"
:title="packageEntity.name"
:avatar="packageIcon"
- data-qa-selector="package_title"
>
<template #sub-header>
<div data-testid="sub-header" class="gl-display-flex gl-flex-wrap gl-gap-3">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
index 482249bc252..0c0001ba6d6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { n__ } from '~/locale';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index a545ad1d09c..674683aa02f 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -183,7 +183,12 @@ export default {
<span data-testid="right-secondary">
<gl-sprintf :message="publishedMessage">
<template v-if="isGroupPage" #projectName>
- <gl-link data-testid="root-link" :href="projectLink">{{ projectName }}</gl-link>
+ <gl-link
+ data-testid="root-link"
+ class="gl-text-decoration-underline"
+ :href="projectLink"
+ >{{ projectName }}</gl-link
+ >
</template>
<template #date>
<timeago-tooltip :time="packageEntity.createdAt" />
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
index 8ecf433f3ab..2f74de9a615 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue
@@ -39,9 +39,12 @@ export default {
<span data-testid="pipeline-ref" class="gl-mr-2">{{ pipeline.ref }}</span>
<gl-icon name="commit" class="gl-mr-2" />
- <gl-link data-testid="pipeline-sha" :href="pipeline.commitPath" class="gl-mr-2">{{
- packageShaShort
- }}</gl-link>
+ <gl-link
+ data-testid="pipeline-sha"
+ :href="pipeline.commitPath"
+ class="gl-mr-2 gl-text-decoration-underline"
+ >{{ packageShaShort }}</gl-link
+ >
<clipboard-button
:text="pipeline.sha"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index a187c7a70d2..294c6baad1b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -171,7 +171,7 @@ export default {
/>
</template>
</package-title>
- <package-search class="gl-mb-5" @update="handleSearchUpdate" />
+ <package-search @update="handleSearchUpdate" />
<delete-packages
:refetch-queries="refetchQueriesData"
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
index de087a8fcc5..e15f204dc6e 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
@@ -3,6 +3,7 @@ import { GlTableLite, GlToggle } from '@gitlab/ui';
import {
GENERIC_PACKAGE_FORMAT,
MAVEN_PACKAGE_FORMAT,
+ NUGET_PACKAGE_FORMAT,
PACKAGE_FORMATS_TABLE_HEADER,
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
@@ -91,6 +92,18 @@ export default {
},
testid: 'generic-settings',
},
+ {
+ id: 'nuget-duplicated-settings-regex-input',
+ format: NUGET_PACKAGE_FORMAT,
+ duplicatesAllowed: this.packageSettings.nugetDuplicatesAllowed,
+ duplicateExceptionRegex: this.packageSettings.nugetDuplicateExceptionRegex,
+ duplicateExceptionRegexError: this.errors.nugetDuplicateExceptionRegex,
+ modelNames: {
+ allowed: 'nugetDuplicatesAllowed',
+ exception: 'nugetDuplicateExceptionRegex',
+ },
+ testid: 'nuget-settings',
+ },
];
},
},
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index bfb57e3ac1c..54b337a4296 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -10,6 +10,7 @@ export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven');
export const NPM_PACKAGE_FORMAT = s__('PackageRegistry|npm');
export const PYPI_PACKAGE_FORMAT = s__('PackageRegistry|PyPI');
export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic');
+export const NUGET_PACKAGE_FORMAT = s__('PackageRegistry|NuGet');
export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
index 267e40263f2..0e36f48e9a6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
@@ -3,6 +3,8 @@ fragment PackageSettingsFields on PackageSettings {
mavenDuplicateExceptionRegex
genericDuplicatesAllowed
genericDuplicateExceptionRegex
+ nugetDuplicatesAllowed
+ nugetDuplicateExceptionRegex
mavenPackageRequestsForwarding
lockMavenPackageRequestsForwarding
mavenPackageRequestsForwardingLocked
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 0a94f67ea5e..18e95ee313e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
@@ -47,7 +47,7 @@ export default {
</script>
<template>
- <div data-qa-selector="package_path" class="gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
<gl-link
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
index f67bee77eb6..ac83f5fc1ad 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
@@ -92,7 +92,7 @@ export default {
<div>
<div
v-if="!hiddenDelete"
- class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center"
+ class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-5 gl-align-items-center"
>
<div class="gl-display-flex gl-align-items-center">
<gl-form-checkbox
diff --git a/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js b/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js
new file mode 100644
index 00000000000..e3524bfbee3
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/appearances/preview_sign_in/index.js
@@ -0,0 +1,3 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
index a5305777dd5..6ca9f39842a 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -1,3 +1,5 @@
import setup from '~/admin/application_settings/setup_metrics_and_profiling';
+import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
setup();
+initServiceUsageData();
diff --git a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js b/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
deleted file mode 100644
index 8a12e753847..00000000000
--- a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
-
-initServiceUsageData();
diff --git a/app/assets/javascripts/pages/explore/catalog/index.js b/app/assets/javascripts/pages/explore/catalog/index.js
new file mode 100644
index 00000000000..fec738a93a6
--- /dev/null
+++ b/app/assets/javascripts/pages/explore/catalog/index.js
@@ -0,0 +1,3 @@
+import { initCatalog } from '~/ci/catalog/';
+
+initCatalog();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/details/index.js b/app/assets/javascripts/pages/import/bulk_imports/details/index.js
new file mode 100644
index 00000000000..5c2571af60f
--- /dev/null
+++ b/app/assets/javascripts/pages/import/bulk_imports/details/index.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import BulkImportDetailsApp from '~/import/details/components/bulk_import_details_app.vue';
+
+export const initBulkImportDetails = () => {
+ const el = document.querySelector('.js-bulk-import-details');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'BulkImportDetailsRoot',
+ render(createElement) {
+ return createElement(BulkImportDetailsApp);
+ },
+ });
+};
+
+initBulkImportDetails();
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 459546a5562..e912bfa4f92 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
@@ -14,13 +14,14 @@ import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { getBulkImportsHistory } from '~/rest_api';
-import ImportStatus from '~/import_entities/components/import_status.vue';
+import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isImporting } from '../utils';
import { DEFAULT_ERROR } from '../utils/error_messages';
@@ -57,6 +58,8 @@ export default {
GlTooltip,
},
+ mixins: [glFeatureFlagMixin()],
+
inject: ['realtimeChangesPath'],
data() {
@@ -103,6 +106,10 @@ export default {
.filter((item) => isImporting(item.status))
.map((item) => item.bulk_import_id);
},
+
+ showDetailsLink() {
+ return this.glFeatures.bulkImportDetailsPage;
+ },
},
watch: {
@@ -225,12 +232,7 @@ export default {
:description="s__('BulkImport|Your imported groups and projects will appear here.')"
/>
<template v-else>
- <gl-table-lite
- :fields="$options.fields"
- :items="historyItems"
- data-qa-selector="import_history_table"
- class="gl-w-full"
- >
+ <gl-table-lite :fields="$options.fields" :items="historyItems" class="gl-w-full">
<template #cell(destination_name)="{ item }">
<gl-icon
v-gl-tooltip
@@ -252,14 +254,23 @@ export default {
<time-ago :time="value" />
</template>
<template #cell(status)="{ value, item, toggleDetails, detailsShowing }">
- <import-status :status="value" class="gl-display-inline-block gl-w-13" />
- <gl-button
- v-if="item.failures.length"
- class="gl-ml-3"
- :selected="detailsShowing"
- @click="toggleDetails"
- >{{ __('Details') }}</gl-button
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-flex-start gl-justify-content-space-between gl-gap-3"
>
+ <import-status
+ :id="item.bulk_import_id"
+ :entity-id="item.id"
+ :has-failures="item.has_failures"
+ :show-details-link="showDetailsLink"
+ :status="value"
+ />
+ <gl-button
+ v-if="!showDetailsLink && item.failures.length"
+ :selected="detailsShowing"
+ @click="toggleDetails"
+ >{{ __('Details') }}</gl-button
+ >
+ </div>
</template>
<template #row-details="{ item }">
<pre><code>{{ item.failures }}</code></pre>
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/index.js b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
index cc12723572d..ac975db3667 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/index.js
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/index.js
@@ -4,13 +4,14 @@ import BulkImportHistoryApp from './components/bulk_imports_history_app.vue';
function mountImportHistoryApp(mountElement) {
if (!mountElement) return undefined;
- const { realtimeChangesPath } = mountElement.dataset;
+ const { realtimeChangesPath, detailsPath } = mountElement.dataset;
return new Vue({
el: mountElement,
name: 'BulkImportHistoryRoot',
provide: {
realtimeChangesPath,
+ detailsPath,
},
render(createElement) {
return createElement(BulkImportHistoryApp);
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index 938c2be89c5..9c0f937fe0e 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -11,11 +11,8 @@ import { DEFAULT_ERROR } from '../utils/error_messages';
import ImportErrorDetails from './import_error_details.vue';
const DEFAULT_PER_PAGE = 20;
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1! gl-p-5!';
const tableCell = (config) => ({
- thClass: DEFAULT_TH_CLASSES,
tdClass: (value, key, item) => {
return {
// eslint-disable-next-line no-underscore-dangle
@@ -57,12 +54,12 @@ export default {
tableCell({
key: 'source',
label: s__('BulkImport|Source'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-30p`,
+ thClass: 'gl-w-30p',
}),
tableCell({
key: 'destination',
label: s__('BulkImport|Destination'),
- thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
+ thClass: 'gl-w-40p',
}),
tableCell({
key: 'created_at',
@@ -144,12 +141,7 @@ export default {
:description="s__('BulkImport|Your imported projects will appear here.')"
/>
<template v-else>
- <gl-table
- :fields="$options.fields"
- :items="historyItems"
- data-qa-selector="import_history_table"
- class="gl-w-full"
- >
+ <gl-table :fields="$options.fields" :items="historyItems" class="gl-w-full">
<template #cell(source)="{ item }">
<template v-if="item.import_url">
<gl-link
diff --git a/app/assets/javascripts/pages/organizations/organizations/users/index.js b/app/assets/javascripts/pages/organizations/organizations/users/index.js
new file mode 100644
index 00000000000..12d53207b22
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/users/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsUsers } from '~/organizations/users';
+
+initOrganizationsUsers();
diff --git a/app/assets/javascripts/pages/organizations/settings/general/index.js b/app/assets/javascripts/pages/organizations/settings/general/index.js
new file mode 100644
index 00000000000..5b74af6206e
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/settings/general/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsSettingsGeneral } from '~/organizations/settings/general';
+
+initOrganizationsSettingsGeneral();
diff --git a/app/assets/javascripts/pages/passwords/new/index.js b/app/assets/javascripts/pages/passwords/new/index.js
new file mode 100644
index 00000000000..e3524bfbee3
--- /dev/null
+++ b/app/assets/javascripts/pages/passwords/new/index.js
@@ -0,0 +1,3 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/profiles/preferences/show/index.js b/app/assets/javascripts/pages/profiles/preferences/show/index.js
index 76939434680..3668811bec7 100644
--- a/app/assets/javascripts/pages/profiles/preferences/show/index.js
+++ b/app/assets/javascripts/pages/profiles/preferences/show/index.js
@@ -1,5 +1,7 @@
import initProfilePreferences from '~/profile/preferences/profile_preferences_bundle';
import initProfilePreferencesDiffsColors from '~/profile/preferences/profile_preferences_diffs_colors';
+import { initHomeOrganizationSetting } from '~/organizations/profile/preferences';
initProfilePreferences();
initProfilePreferencesDiffsColors();
+initHomeOrganizationSetting();
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 07662e4411e..d42fb10063e 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -153,9 +153,10 @@ const initForkInfo = () => {
initForkInfo();
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
-const statusLink = document.querySelector('.commit-actions .ci-status-link');
-if (statusLink) {
- statusLink.remove();
+const legacyStatusBadge = document.querySelector('.js-ci-status-badge-legacy');
+
+if (legacyStatusBadge) {
+ legacyStatusBadge.remove();
// eslint-disable-next-line no-new
new Vue({
el: CommitPipelineStatusEl,
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 9659c927fbf..e3d50e900ca 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -7,6 +7,7 @@ import {
GlFormGroup,
GlFormTextarea,
GlButton,
+ GlSprintf,
GlFormRadio,
GlFormRadioGroup,
} from '@gitlab/ui';
@@ -56,6 +57,7 @@ export default {
GlIcon,
GlLink,
GlButton,
+ GlSprintf,
GlFormInput,
GlFormTextarea,
GlFormGroup,
@@ -91,6 +93,9 @@ export default {
projectDescription: {
default: '',
},
+ projectDefaultBranch: {
+ default: '',
+ },
projectVisibility: {
default: '',
},
@@ -116,6 +121,7 @@ export default {
required: false,
skipValidation: true,
}),
+ branches: initFormField({ value: '', required: true, skipValidation: true }),
visibility: initFormField({ value: null }),
},
};
@@ -168,6 +174,18 @@ export default {
return allowedLevels;
},
+ branchesOptions() {
+ return [
+ {
+ text: s__('ForkProject|All branches'),
+ value: '',
+ },
+ {
+ text: s__(`ForkProject|Only the default branch %{defaultBranch}`),
+ value: this.projectDefaultBranch,
+ },
+ ];
+ },
visibilityLevels() {
return [
{
@@ -245,7 +263,7 @@ export default {
this.form.showValidation = false;
const { projectId } = this;
- const { name, slug, description, visibility, namespace } = this.form.fields;
+ const { name, slug, description, branches, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
@@ -253,6 +271,7 @@ export default {
namespace_id: namespace.value.id,
path: slug.value,
description: description.value,
+ branches: branches.value,
visibility: visibility.value,
};
@@ -263,6 +282,7 @@ export default {
const { data } = await axios.post(url, postParams);
redirectTo(data.web_url); // eslint-disable-line import/no-deprecated
} catch (error) {
+ this.isSaving = false;
createAlert({
message: s__(
'ForkProject|An error occurred while forking the project. Please try again.',
@@ -348,6 +368,34 @@ export default {
/>
</gl-form-group>
+ <gl-form-group>
+ <label>
+ {{ s__('ForkProject|Branches to include') }}
+ </label>
+ <gl-form-radio-group
+ v-model="form.fields.branches.value"
+ data-testid="fork-branches-radio-group"
+ name="branches"
+ :aria-label="__('branches')"
+ required
+ >
+ <gl-form-radio
+ v-for="{ text, value } in branchesOptions"
+ :key="value"
+ :value="value"
+ :data-testid="`radio-${value}`"
+ >
+ <div>
+ <gl-sprintf :message="text">
+ <template #defaultBranch>
+ <code class="gl-ml-2">{{ projectDefaultBranch }}</code>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ </gl-form-group>
+
<gl-form-group
v-validation:[form.showValidation]
:invalid-feedback="s__('ForkProject|Please select a visibility level')"
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index a31b8b1a1f4..694914e9154 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -15,6 +15,7 @@ const {
projectId,
projectName,
projectPath,
+ projectDefaultBranch,
projectDescription,
projectVisibility,
restrictedVisibilityLevels,
@@ -38,6 +39,7 @@ new Vue({
projectName,
projectPath,
projectDescription,
+ projectDefaultBranch,
projectVisibility,
restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels),
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index fb243d01dc6..a9d281fc899 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -28,7 +28,7 @@ requestIdleCallback(() => {
if (el) {
const { data } = el.dataset;
- const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
+ const { iid, projectPath, title, tabs, isFluidLayout, sourceProjectPath } = JSON.parse(data);
// eslint-disable-next-line no-new
new Vue({
@@ -42,6 +42,7 @@ requestIdleCallback(() => {
title,
tabs,
isFluidLayout: parseBoolean(isFluidLayout),
+ sourceProjectPath,
},
render(h) {
return h(StickyHeader);
diff --git a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
new file mode 100644
index 00000000000..1a2b85d7e16
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import { ShowMlModelVersion } from '~/ml/model_registry/apps';
+
+initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion);
diff --git a/app/assets/javascripts/pages/projects/ml/models/index/index.js b/app/assets/javascripts/pages/projects/ml/models/index/index.js
index 62d326f43a5..3f8ef4910a7 100644
--- a/app/assets/javascripts/pages/projects/ml/models/index/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/index/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import MlModelsIndex from '~/ml/model_registry/routes/models/index';
+import { IndexMlModels } from '~/ml/model_registry/apps';
-initSimpleApp('#js-index-ml-models', MlModelsIndex);
+initSimpleApp('#js-index-ml-models', IndexMlModels);
diff --git a/app/assets/javascripts/pages/projects/ml/models/show/index.js b/app/assets/javascripts/pages/projects/ml/models/show/index.js
index 87ee5c851f6..c8e25e0f0e8 100644
--- a/app/assets/javascripts/pages/projects/ml/models/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/show/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { ShowMlModel } from '~/ml/model_registry/apps';
-initSimpleApp('#js-mount-show-ml-model', ShowMlModel);
+initSimpleApp('#js-mount-show-ml-model', ShowMlModel, { withApolloProvider: true });
diff --git a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js b/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
deleted file mode 100644
index ba03fccdb03..00000000000
--- a/app/assets/javascripts/pages/projects/product_analytics/graphs/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initActivityCharts from '~/analytics/product_analytics/activity_charts_bundle';
-
-initActivityCharts();
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 90a9c9e7279..54974e878c3 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -5,6 +5,7 @@ import EmailFormatValidator from '~/pages/sessions/new/email_format_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import { initPasswordInput } from '~/authentication/password';
import Tracking from '~/tracking';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
@@ -17,3 +18,4 @@ Tracking.enableFormTracking({
initLanguageSwitcher();
initPasswordInput();
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 1d5d885753c..32df2911a48 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -4,6 +4,7 @@ import NoEmojiValidator from '~/emoji/no_emoji_validator';
import { initLanguageSwitcher } from '~/language_switcher';
import LengthValidator from '~/validators/length_validator';
import mountEmailVerificationApplication from '~/sessions/new';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
import SigninTabsMemoizer from './signin_tabs_memoizer';
@@ -24,3 +25,4 @@ preserveUrlFragment(window.location.hash);
initVueAlerts();
initLanguageSwitcher();
mountEmailVerificationApplication();
+renderGFM(document.body);
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index af55a5dc01a..d2c31314bba 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,11 +1,15 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
-import { initReportAbuse } from '~/users/profile';
-import { initProfileTabs } from '~/profile';
+import { initProfileTabs, initUserAchievements } from '~/profile';
+import { initUserActionsApp } from '~/users/profile/actions';
import UserTabs from './user_tabs';
function initUserProfile(action) {
+ // TODO: Remove both Vue and legacy JS tabs code/feature flag uses with the
+ // removal of the old navigation.
+ // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
if (gon.features?.profileTabsVue) {
initProfileTabs();
} else {
@@ -24,5 +28,6 @@ function initUserProfile(action) {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
+initUserAchievements();
+initUserActionsApp();
new UserCallout(); // eslint-disable-line no-new
-initReportAbuse();
diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js
deleted file mode 100644
index 7d612d6cc4e..00000000000
--- a/app/assets/javascripts/pages/users/show/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { initUserAchievements } from '~/profile';
-import { initUserActionsApp } from '~/users/profile/actions';
-
-initUserAchievements();
-initUserActionsApp();
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 430022f9a9b..79eb3902116 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,3 +1,6 @@
+// TODO: Remove this with the removal of the old navigation.
+// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Activities from '~/activities';
@@ -194,7 +197,7 @@ export default class UserTabs {
this.loadActivityCalendar();
UserTabs.renderMostRecentBlocks('#js-overview .activities-block', {
- requestParams: { limit: 10 },
+ requestParams: { limit: 15 },
});
UserTabs.renderMostRecentBlocks('#js-overview .projects-block', {
requestParams: { limit: 10, skip_pagination: true, skip_namespace: true, compact_mode: true },
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 720c1e0d7f2..c5f8fd1904f 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -61,11 +61,6 @@ export default {
keys: ['feature', 'request'],
},
{
- metric: 'rugged',
- header: s__('PerformanceBar|Rugged calls'),
- keys: ['feature', 'args'],
- },
- {
metric: 'redis',
header: s__('PerformanceBar|Redis calls'),
keys: ['cmd', 'instance'],
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index cea01852630..bba8e1f7ba5 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -23,7 +23,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
- '.js-new-navigation-callout',
+ '.js-new-nav-for-everyone-callout',
'.js-namespace-over-storage-users-combined-alert',
];
diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
index ab837d04d9a..43e70046cfb 100644
--- a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
@@ -14,8 +14,7 @@ import CommitStep from './commit.vue';
export const i18n = {
stepNofN: __('Step %{currentStep} of %{stepCount}'),
draft: __('Draft: %{filename}'),
- overlayMessage: __(`Start inputting changes and we will generate a
- YAML-file for you to add to your repository`),
+ overlayMessage: __(`Enter values to populate the .gitlab-ci.yml configuration file.`),
};
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
index 5a93de3b1be..3676ba96254 100644
--- a/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
+++ b/app/assets/javascripts/pipeline_wizard/pipeline_wizard.vue
@@ -1,5 +1,6 @@
<script>
import { parseDocument } from 'yaml';
+import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants';
import WizardWrapper from './components/wrapper.vue';
export default {
@@ -23,7 +24,7 @@ export default {
defaultFilename: {
type: String,
required: false,
- default: '.gitlab-ci.yml',
+ default: DEFAULT_CI_CONFIG_PATH,
},
},
computed: {
diff --git a/app/assets/javascripts/pipeline_wizard/templates/pages.yml b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
index 9d7936f2f5a..8eecd51fe27 100644
--- a/app/assets/javascripts/pipeline_wizard/templates/pages.yml
+++ b/app/assets/javascripts/pipeline_wizard/templates/pages.yml
@@ -1,12 +1,10 @@
id: gitlab/pages
-title: Get started with Pages
-description: "GitLab Pages lets you deploy static websites in minutes. All you
- need is a .gitlab-ci.yml file. Follow the below steps to
- create one for your app now."
+title: Get started with GitLab Pages
+description: "Use GitLab Pages to deploy your static website. Follow these steps to create the configuration file, .gitlab-ci.yml, and start a pipeline to deploy the site."
steps:
- inputs:
- label: Select your build image
- description: A Docker image that we can use to build your image
+ description: A Docker image, used to create an instance where your job runs.
placeholder: node:lts
widget: text
target: $BUILD_IMAGE
@@ -14,18 +12,15 @@ steps:
pattern: "(?:[a-z]+/)?([a-z]+)(?::[0-9]+)?"
invalid-feedback: Please enter a valid docker image
- widget: checklist
- title: "Before we begin, please check:"
items:
- - text: The app's built output files are in a folder named "public"
- help: GitLab Pages will only publish files in that folder.
- You may need to adjust your build engine's config.
+ - text: The application files are in the `public` folder
+ help: GitLab Pages publishes files in the public folder only. If needed, change your jobs to send output to this folder.
template:
# The Docker image that will be used to build your app
image: $BUILD_IMAGE
- inputs:
- label: Installation Steps
- description: "Enter the steps that need to run to set up a local build
- environment, for example installing dependencies."
+ description: "Enter steps to set up a local build environment, like installing dependencies."
placeholder: npm ci
widget: list
target: $INSTALLATION_STEPS
@@ -34,8 +29,7 @@ steps:
before_script: $INSTALLATION_STEPS
- inputs:
- label: Build Steps
- description: "Enter the steps necessary to build a production version of
- your application."
+ description: "Enter steps to build a production version of your application."
widget: list
target: $BUILD_STEPS
template:
diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
index 6b39f137880..815b8742500 100644
--- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
+++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
@@ -109,8 +109,11 @@ export default {
async syncHeaderAvatars() {
const dataURL = await readFileAsDataURL(this.avatarBlob);
- // TODO: implement sync for super sidebar
- ['.header-user-avatar', '.js-sidebar-user-avatar'].forEach((selector) => {
+ const elements = gon?.use_new_navigation
+ ? ['[data-testid="user-dropdown"] .gl-avatar']
+ : ['.header-user-avatar', '.js-sidebar-user-avatar'];
+
+ elements.forEach((selector) => {
const node = document.querySelector(selector);
if (!node) return;
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index aa30192b74b..2fc1f99c183 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -5,9 +5,9 @@ import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
function updateClasses(bodyClasses = '', applicationTheme, layout) {
- // Remove body class for any previous theme, re-add current one
- document.body.classList.remove(...bodyClasses.split(' '));
- document.body.classList.add(applicationTheme);
+ // Remove documentElement class for any previous theme, re-add current one
+ document.documentElement.classList.remove(...bodyClasses.split(' '));
+ document.documentElement.classList.add(applicationTheme);
// Toggle container-fluid class
if (layout === 'fluid') {
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 947bf7acd5c..2ccb360c7c1 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -89,8 +89,12 @@ export default class Profile {
}
updateHeaderAvatar() {
- $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ if (gon?.use_new_navigation) {
+ $('[data-testid="user-dropdown"] .gl-avatar').attr('src', this.avatarGlCrop.dataURL);
+ } else {
+ $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
+ }
}
setRepoRadio() {
diff --git a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
index 7c00ce45b3a..377310b087e 100644
--- a/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/commit_options_dropdown.vue
@@ -51,7 +51,6 @@ export default {
text: s__('ChangeTypeAction|Cherry-pick'),
extraAttrs: {
'data-testid': 'cherry-pick-link',
- 'data-qa-selector': 'cherry_pick_button',
},
action: () => this.showModal(OPEN_CHERRY_PICK_MODAL),
};
@@ -62,7 +61,6 @@ export default {
text: s__('ChangeTypeAction|Revert'),
extraAttrs: {
'data-testid': 'revert-link',
- 'data-qa-selector': 'revert_button',
},
action: () => this.showModal(OPEN_REVERT_MODAL),
};
@@ -85,7 +83,6 @@ export default {
download: '',
rel: 'nofollow',
'data-testid': 'plain-diff-link',
- 'data-qa-selector': 'plain_diff',
},
};
},
@@ -97,7 +94,6 @@ export default {
download: '',
rel: 'nofollow',
'data-testid': 'email-patches-link',
- 'data-qa-selector': 'email_patches',
},
};
},
@@ -148,8 +144,7 @@ export default {
:toggle-text="__('Options')"
right
data-testid="commit-options-dropdown"
- data-qa-selector="options_button"
- class="gl-xs-w-full gl-line-height-20"
+ class="gl-line-height-20"
>
<gl-disclosure-dropdown-group :group="optionsGroup" @action="closeDropdown" />
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 44b8ccb57ca..d1e78084b9f 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -57,7 +57,6 @@ export default {
variant: 'confirm',
category: 'primary',
'data-testid': 'submit-commit',
- 'data-qa-selector': 'submit_commit_button',
},
},
actionCancel: {
@@ -74,7 +73,6 @@ export default {
'branchCollaboration',
'modalTitle',
'existingBranch',
- 'prependedText',
'targetProjectId',
'targetProjectName',
'branchesEndpoint',
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 0feaf8db82b..a4851b4fe4b 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 } from '@gitlab/ui';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.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,7 +9,7 @@ import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../consta
export default {
PIPELINE_STATUS_FETCH_ERROR,
components: {
- CiBadgeLink,
+ CiIcon,
GlLoadingIcon,
},
inject: {
@@ -63,12 +63,6 @@ export default {
<template>
<div class="gl-display-inline-block gl-vertical-align-middle gl-mr-2">
<gl-loading-icon v-if="loading" />
- <ci-badge-link
- v-else
- :status="pipelineStatus"
- :details-path="pipelineStatus.detailsPath"
- size="md"
- :show-text="false"
- />
+ <ci-icon v-else :status="pipelineStatus" />
</div>
</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index c206e648561..24b7130e765 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,7 +5,7 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { useGet: true }),
+ defaultClient: createDefaultClient(),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
index d5e62531283..079f74dc8a2 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_status.js
@@ -6,7 +6,7 @@ import CommitBoxPipelineStatus from './components/commit_box_pipeline_status.vue
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient({}, { useGet: true }),
+ defaultClient: createDefaultClient(),
});
export default (selector = '.js-commit-pipeline-status') => {
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 5175f7f9151..7d04e9a15a3 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 5bbc881952f..e3599c87616 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -121,4 +121,8 @@ export default {
text: s__('ProjectTemplates|Laravel Framework'),
icon: '.template-option .icon-laravel',
},
+ astro_tailwind: {
+ text: s__('ProjectTemplates|Astro Tailwind'),
+ icon: '.template-option .icon-gitlab_logo',
+ },
};
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index ef2a2aa5526..84a2ddfce07 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -67,7 +67,7 @@ export default {
}
: this.$options.emptyNameSpace,
shouldSkipQuery: true,
- userNamespaceId: this.userNamespaceId,
+ userNamespaceUniqueId: this.userNamespaceId,
};
},
computed: {
@@ -186,7 +186,7 @@ export default {
{{ group.fullPath }}
</gl-dropdown-item>
</template>
- <template v-if="hasNamespaceMatches && userNamespaceId">
+ <template v-if="hasNamespaceMatches && userNamespaceUniqueId">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
@@ -202,7 +202,7 @@ export default {
:id="inputId"
type="hidden"
:name="inputName"
- :value="selectedNamespace.id || userNamespaceId"
+ :value="selectedNamespace.id || userNamespaceUniqueId"
/>
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
index 5383a6cdddf..f921b2dfdd6 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/statistics_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink } from '@gitlab/ui';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
-import { s__, n__ } from '~/locale';
+import { s__, n__, formatNumber } from '~/locale';
const defaultPrecision = 2;
@@ -22,25 +22,25 @@ export default {
},
computed: {
statistics() {
- const formatter = getFormatter(SUPPORTED_FORMATS.percentHundred);
+ const formatPercent = getFormatter(SUPPORTED_FORMATS.percentHundred);
return [
{
title: s__('PipelineCharts|Total:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.total),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.total)),
},
{
title: s__('PipelineCharts|Successful:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.success),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.success)),
},
{
title: s__('PipelineCharts|Failed:'),
- value: n__('1 pipeline', '%d pipelines', this.counts.failed),
+ value: n__('1 pipeline', '%d pipelines', formatNumber(this.counts.failed)),
link: this.failedPipelinesLink,
},
{
title: s__('PipelineCharts|Success ratio:'),
- value: formatter(this.counts.successRatio, defaultPrecision),
+ value: formatPercent(this.counts.successRatio, defaultPrecision),
},
];
},
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index dbcb77b67f3..becd373c5f1 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -187,7 +187,7 @@ export default {
:roles="pushAccessLevels.roles"
:users="pushAccessLevels.users"
:groups="pushAccessLevels.groups"
- data-qa-selector="allowed_to_push_content"
+ data-testid="allowed-to-push-content"
/>
<!-- Allowed to merge -->
@@ -198,7 +198,7 @@ export default {
:roles="mergeAccessLevels.roles"
:users="mergeAccessLevels.users"
:groups="mergeAccessLevels.groups"
- data-qa-selector="allowed_to_merge_content"
+ data-testid="allowed-to-merge-content"
/>
<!-- Force push -->
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
index 3a5b3409596..366c69556f2 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -105,7 +105,6 @@ export default {
v-for="(item, index) in accessLevels"
:key="index"
data-testid="access-level"
- data-qa-selector="access_level_content"
:data-qa-role="item.accessLevelDescription"
>
<span v-if="commaSeparateList && index > 0" data-testid="comma-separator">,</span>
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
index fee2f591216..f5fb72e84bc 100644
--- a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -33,6 +33,5 @@ export default {
:translations="$options.i18n"
name="project[default_branch]"
data-testid="default-branch-dropdown"
- data-qa-selector="default_branch_dropdown"
/>
</template>
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 67afbee3854..b02a33675ee 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -1,5 +1,5 @@
-import * as Sentry from '@sentry/browser';
import Vue from 'vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessDropdown from './components/access_dropdown.vue';
export const initAccessDropdown = (el, options) => {
@@ -22,6 +22,7 @@ export const initAccessDropdown = (el, options) => {
data() {
return { preselected };
},
+ disabled,
methods: {
setPreselectedItems(items) {
this.preselected = items;
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 7753b850744..7d9ad83a1c6 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -76,7 +76,7 @@ export default {
v-gl-modal="$options.modalId"
size="small"
class="gl-ml-3"
- data-qa-selector="add_branch_rule_button"
+ data-testid="add-branch-rule-button"
>{{ $options.i18n.addBranchRule }}</gl-button
>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index f45a5b12db6..0a5fa288828 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -156,7 +156,7 @@ export default {
<li>
<div
class="gl-display-flex gl-justify-content-space-between"
- data-qa-selector="branch_content"
+ data-testid="branch-content"
:data-qa-branch-name="name"
>
<div>
@@ -178,7 +178,7 @@ export default {
class="gl-align-self-start"
category="tertiary"
size="small"
- data-qa-selector="details_button"
+ data-testid="details-button"
:href="detailsPath"
>
{{ $options.i18n.detailsButtonLabel }}</gl-button
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
index 09bc275cbd4..6f22af4bd26 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue
@@ -6,10 +6,16 @@ import {
GlFormInputGroup,
GlFormInput,
GlLink,
+ GlFormSelect,
GlSprintf,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
+import {
+ isEmptyValue,
+ hasMinimumLength,
+ isIntegerGreaterThan,
+ isServiceDeskSettingEmail,
+} from '~/lib/utils/forms';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
I18N_FORM_INTRODUCTION_PARAGRAPH,
@@ -23,6 +29,11 @@ import {
I18N_FORM_SMTP_USERNAME_LABEL,
I18N_FORM_SMTP_PASSWORD_LABEL,
I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SMTP_AUTHENTICATION_LABEL,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
I18N_FORM_SUBMIT_LABEL,
I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
@@ -42,6 +53,7 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlFormInput,
+ GlFormSelect,
GlLink,
GlSprintf,
},
@@ -56,6 +68,11 @@ export default {
I18N_FORM_SMTP_USERNAME_LABEL,
I18N_FORM_SMTP_PASSWORD_LABEL,
I18N_FORM_SMTP_PASSWORD_DESCRIPTION,
+ I18N_FORM_SMTP_AUTHENTICATION_LABEL,
+ I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
I18N_FORM_SUBMIT_LABEL,
I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL,
I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS,
@@ -82,6 +99,7 @@ export default {
smtpPort: '587',
smtpUsername: '',
smtpPassword: '',
+ smtpAuthentication: null,
validationState: {
customEmail: null,
smtpAddress: null,
@@ -113,6 +131,7 @@ export default {
smtp_port: this.smtpPort,
smtp_username: this.smtpUsername,
smtp_password: this.smtpPassword,
+ smtp_authentication: this.smtpAuthentication,
};
},
onCustomEmailChange() {
@@ -124,7 +143,7 @@ export default {
}
},
validateCustomEmail() {
- this.validationState.customEmail = isEmail(this.customEmail);
+ this.validationState.customEmail = isServiceDeskSettingEmail(this.customEmail);
},
validateSmtpAddress() {
this.validationState.smtpAddress = !isEmptyValue(this.smtpAddress);
@@ -145,6 +164,26 @@ export default {
this.validateSmtpUsername();
this.validateSmtpPassword();
},
+ getSmtpAuthenticationOptions() {
+ return [
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_NONE,
+ value: null,
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_PLAIN,
+ value: 'plain',
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_LOGIN,
+ value: 'login',
+ },
+ {
+ text: this.$options.I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5,
+ value: 'cram_md5',
+ },
+ ];
+ },
},
};
</script>
@@ -298,6 +337,20 @@ export default {
/>
</gl-form-group>
+ <gl-form-group
+ :label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL"
+ label-for="custom-email-form-smtp-password"
+ class="gl-mt-3"
+ >
+ <gl-form-select
+ id="custom-email-form-smtp-authentication"
+ v-model.trim="smtpAuthentication"
+ :options="getSmtpAuthenticationOptions()"
+ :aria-label="$options.I18N_FORM_SMTP_AUTHENTICATION_LABEL"
+ :disabled="isSubmitting"
+ />
+ </gl-form-group>
+
<gl-button
type="submit"
variant="confirm"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
index 03ba99bcf71..f72aa19bdf2 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -234,7 +234,6 @@ export default {
:href="$options.FEEDBACK_ISSUE_URL"
target="_blank"
data-testid="feedback-link"
- class="gl-text-blue-600 font-size-inherit"
>{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 2b2722ab329..6674937be67 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -55,6 +55,9 @@ export default {
projectKey: {
default: '',
},
+ addExternalParticipantsFromCc: {
+ default: false,
+ },
templates: {
default: [],
},
@@ -109,13 +112,20 @@ export default {
});
},
- onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) {
+ onSaveTemplate({
+ selectedTemplate,
+ fileTemplateProjectId,
+ outgoingName,
+ projectKey,
+ addExternalParticipantsFromCc,
+ }) {
this.isTemplateSaving = true;
const body = {
issue_template_key: selectedTemplate,
outgoing_name: outgoingName,
project_key: projectKey,
+ add_external_participants_from_cc: addExternalParticipantsFromCc,
service_desk_enabled: this.isEnabled,
file_template_project_id: fileTemplateProjectId,
};
@@ -187,6 +197,7 @@ export default {
:initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
+ :initial-add-external-participants-from-cc="addExternalParticipantsFromCc"
:templates="templates"
:is-template-saving="isTemplateSaving"
@save="onSaveTemplate"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 5078cbbdf59..5febb6ff0aa 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -4,6 +4,7 @@ import {
GlToggle,
GlLoadingIcon,
GlSprintf,
+ GlFormCheckbox,
GlFormInputGroup,
GlFormGroup,
GlFormInput,
@@ -11,7 +12,8 @@ import {
GlAlert,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue';
@@ -21,12 +23,22 @@ export default {
issueTrackerEnableMessage: __(
'To use Service Desk in this project, you must %{linkStart}activate the issue tracker%{linkEnd}.',
),
+ addExternalParticipantsFromCc: {
+ label: s__('ServiceDesk|Add external participants from the %{codeStart}Cc%{codeEnd} header'),
+ help: s__(
+ 'ServiceDesk|Add email addresses in the %{codeStart}Cc%{codeEnd} header of Service Desk emails to the issue.',
+ ),
+ helpNotificationExtra: s__(
+ 'ServiceDesk|Like the author, external participants receive Service Desk emails and can participate in the discussion.',
+ ),
+ },
},
components: {
ClipboardButton,
GlButton,
GlToggle,
GlLoadingIcon,
+ GlFormCheckbox,
GlSprintf,
GlFormInput,
GlFormGroup,
@@ -35,6 +47,7 @@ export default {
GlAlert,
ServiceDeskTemplateDropdown,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
isEnabled: {
type: Boolean,
@@ -78,6 +91,11 @@ export default {
required: false,
default: '',
},
+ initialAddExternalParticipantsFromCc: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
templates: {
type: Array,
required: false,
@@ -95,11 +113,15 @@ export default {
selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
+ addExternalParticipantsFromCc: this.initialAddExternalParticipantsFromCc,
searchTerm: '',
projectKeyError: null,
};
},
computed: {
+ showAddExternalParticipantsFromCC() {
+ return this.glFeatures.issueEmailParticipants;
+ },
hasProjectKeySupport() {
return Boolean(this.serviceDeskEmailEnabled);
},
@@ -134,6 +156,7 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
+ addExternalParticipantsFromCc: this.addExternalParticipantsFromCc,
fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
},
@@ -240,12 +263,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link
- :href="emailSuffixHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
- >{{ content }}
- </gl-link>
+ <gl-link :href="emailSuffixHelpUrl" target="_blank">{{ content }} </gl-link>
</template>
</gl-sprintf>
</template>
@@ -259,10 +277,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link
- :href="serviceDeskEmailAddressHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
+ <gl-link :href="serviceDeskEmailAddressHelpUrl" target="_blank"
>{{ content }}
</gl-link>
</template>
@@ -307,11 +322,31 @@ export default {
</template>
</gl-form-group>
+ <gl-form-checkbox
+ v-if="showAddExternalParticipantsFromCC"
+ v-model="addExternalParticipantsFromCc"
+ :disabled="!isIssueTrackerEnabled"
+ >
+ <gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.label">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+
+ <template #help>
+ <gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.help">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ {{ $options.i18n.addExternalParticipantsFromCc.helpNotificationExtra }}
+ </template>
+ </gl-form-checkbox>
+
<gl-button
variant="confirm"
class="gl-mt-5"
data-testid="save_service_desk_settings_button"
- data-qa-selector="save_service_desk_settings_button"
:disabled="isTemplateSaving || !isIssueTrackerEnabled"
@click="onSaveTemplate"
>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
index 315f0743b53..86c4fdcc30a 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue
@@ -84,7 +84,6 @@ export default {
id="service-desk-template-select"
:text="selectedTemplate || $options.i18n.defaultDropdownText"
:header-text="$options.i18n.defaultDropdownText"
- data-qa-selector="service_desk_template_dropdown"
:block="true"
class="service-desk-template-select"
toggle-class="gl-m-0"
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index aafd77bd25e..8ac186e292c 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -37,6 +37,13 @@ export const I18N_FORM_SMTP_PORT_DESCRIPTION = s__(
export const I18N_FORM_SMTP_USERNAME_LABEL = s__('ServiceDesk|SMTP username');
export const I18N_FORM_SMTP_PASSWORD_LABEL = s__('ServiceDesk|SMTP password');
export const I18N_FORM_SMTP_PASSWORD_DESCRIPTION = s__('ServiceDesk|Minimum 8 characters long.');
+export const I18N_FORM_SMTP_AUTHENTICATION_LABEL = s__('ServiceDesk|SMTP authentication method');
+export const I18N_FORM_SMTP_AUTHENTICATION_NONE = s__(
+ 'ServiceDesk|Let GitLab select a server-supported method (recommended)',
+);
+export const I18N_FORM_SMTP_AUTHENTICATION_PLAIN = s__('ServiceDesk|Plain');
+export const I18N_FORM_SMTP_AUTHENTICATION_LOGIN = s__('ServiceDesk|Login');
+export const I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5 = s__('ServiceDesk|CRAM-MD5');
export const I18N_FORM_SUBMIT_LABEL = s__('ServiceDesk|Save and test connection');
export const I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL = s__(
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index c4d4f42576f..ce223b349bf 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -21,6 +21,7 @@ export default () => {
incomingEmail,
outgoingName,
projectKey,
+ addExternalParticipantsFromCc,
selectedTemplate,
selectedFileTemplateProjectId,
templates,
@@ -39,6 +40,7 @@ export default () => {
isIssueTrackerEnabled: parseBoolean(issueTrackerEnabled),
outgoingName,
projectKey,
+ addExternalParticipantsFromCc: parseBoolean(addExternalParticipantsFromCc),
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
templates: JSON.parse(templates),
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
index 5b620aa2300..074cddac422 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
@@ -91,17 +91,10 @@ export default {
};
</script>
<template>
- <div class="ci-status-link">
+ <div class="gl-ml-5">
<gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
- <ci-icon
- v-gl-tooltip
- :title="statusTitle"
- :aria-label="statusTitle"
- :status="ciStatus"
- :size="24"
- data-container="body"
- />
+ <ci-icon :status="ciStatus" :title="statusTitle" :aria-label="statusTitle" />
</a>
</div>
</template>
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 29034b3bc0e..66da3de516a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -6,6 +6,10 @@ import { initToggle } from '~/toggles';
import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+const isDropdownDisabled = (dropdown) => {
+ return dropdown?.$options.disabled === '';
+};
+
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
@@ -104,6 +108,9 @@ export default class ProtectedBranchEdit {
}
initSelectedItems(dropdown, accessLevel) {
+ if (isDropdownDisabled(dropdown)) {
+ return;
+ }
this.selectedItems[accessLevel] = dropdown.preselected.map((item) => {
if (item.type === LEVEL_TYPES.USER) return { id: item.id, user_id: item.user_id };
if (item.type === LEVEL_TYPES.ROLE) return { id: item.id, access_level: item.access_level };
@@ -183,7 +190,10 @@ export default class ProtectedBranchEdit {
};
});
- this.selectedItems[accessLevel] = itemsToAdd;
- this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
+ const dropdown = this[`${accessLevel}_dropdown`];
+ if (!isDropdownDisabled(dropdown)) {
+ this.selectedItems[accessLevel] = itemsToAdd;
+ dropdown?.setPreselectedItems(itemsToAdd);
+ }
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index b5661af352c..b3754cecce4 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -41,7 +41,7 @@ export default class ProtectedTagCreate {
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
searchEnabled: dropdownEl.dataset.filter !== undefined,
- testId: 'allowed_to_create_dropdown',
+ testId: 'allowed-to-create-dropdown',
});
this.protectedTagAccessDropdown.$on('select', (selected) => {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
index 82b2ecc5f5c..7fe1dc9c01a 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.vue
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -101,7 +101,7 @@ export default {
<template>
<access-dropdown
toggle-class="js-allowed-to-create gl-max-w-34"
- test-id="allowed_to_create_dropdown"
+ test-id="allowed-to-create-dropdown"
:has-license="hasLicense"
:access-level="$options.ACCESS_LEVELS.CREATE"
:access-levels-data="accessLevelsData"
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
index 444d6e9cf76..fad15a5d89e 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import ProtectedTagEdit from './protected_tag_edit.vue';
export default class ProtectedTagEditList {
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index c68fbceb4f6..df9f333afe5 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -176,8 +176,7 @@ export default {
</p>
<form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm">
<tag-field />
- <gl-form-group>
- <label for="release-title">{{ __('Release title') }}</label>
+ <gl-form-group :label="__('Release title')">
<gl-form-input
id="release-title"
ref="releaseTitleInput"
@@ -186,17 +185,14 @@ export default {
class="form-control"
/>
</gl-form-group>
- <gl-form-group class="w-50" data-testid="milestones-field">
- <label>{{ __('Milestones') }}</label>
- <div class="d-flex flex-column col-md-6 col-sm-10 pl-0">
- <milestone-combobox
- v-model="releaseMilestones"
- :project-id="projectId"
- :group-id="groupId"
- :group-milestones-available="groupMilestonesAvailable"
- :extra-links="milestoneComboboxExtraLinks"
- />
- </div>
+ <gl-form-group :label="__('Milestones')" class="gl-w-30" data-testid="milestones-field">
+ <milestone-combobox
+ v-model="releaseMilestones"
+ :project-id="projectId"
+ :group-id="groupId"
+ :group-milestones-available="groupMilestonesAvailable"
+ :extra-links="milestoneComboboxExtraLinks"
+ />
</gl-form-group>
<gl-form-group :label="__('Release date')" label-for="release-released-at">
<template #label-description>
@@ -214,8 +210,7 @@ export default {
</template>
<gl-datepicker id="release-released-at" v-model="releasedAt" :default-date="releasedAt" />
</gl-form-group>
- <gl-form-group data-testid="release-notes">
- <label for="release-notes">{{ __('Release notes') }}</label>
+ <gl-form-group :label="__('Release notes')" data-testid="release-notes">
<div class="common-note-form">
<markdown-field
:can-attach-file="true"
diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue
index 070865cf84b..b4c897a8236 100644
--- a/app/assets/javascripts/releases/components/release_block_header.vue
+++ b/app/assets/javascripts/releases/components/release_block_header.vue
@@ -50,7 +50,7 @@ export default {
<template>
<div class="card-header d-flex align-items-center bg-white pr-0">
<h2 class="card-title my-2 mr-auto">
- <gl-link v-if="selfLink" :href="selfLink" class="font-size-inherit">
+ <gl-link v-if="selfLink" :href="selfLink">
{{ release.name }}
</gl-link>
<template v-else>
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 99b861ca104..ed7212eb9a6 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -92,8 +92,8 @@ export default {
deleteModalTitle() {
return sprintf(__('Delete %{name}'), { name: this.name });
},
- lockBtnQASelector() {
- return this.canLock ? 'lock_button' : 'disabled_lock_button';
+ lockBtnTestId() {
+ return this.canLock ? 'lock-button' : 'disabled-lock-button';
},
},
methods: {
@@ -120,8 +120,7 @@ export default {
:project-path="projectPath"
:is-locked="isLocked"
:can-lock="canLock"
- data-testid="lock"
- :data-qa-selector="lockBtnQASelector"
+ :data-testid="lockBtnTestId"
/>
<gl-button data-testid="replace" @click="showModal($options.replaceBlobModalId)">
{{ $options.i18n.replace }}
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 6565c84fa11..97a1cbda5d0 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -122,6 +122,7 @@ export default {
blobInfo: {},
isEmptyRepository: false,
projectId: null,
+ showBlame: this.$route?.query?.blame === '1',
};
},
computed: {
@@ -202,6 +203,9 @@ export default {
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
+ isBlameEnabled() {
+ return this.glFeatures.blobBlameInfo && this.blobInfo.language === 'json'; // This feature is currently scoped to JSON files
+ },
},
watch: {
// Watch the URL 'plain' query value to know if the viewer needs changing.
@@ -289,6 +293,14 @@ export default {
onCopy() {
navigator.clipboard.writeText(this.blobInfo.rawTextBlob);
},
+ handleToggleBlame() {
+ this.switchViewer(SIMPLE_BLOB_VIEWER);
+ this.showBlame = !this.showBlame;
+
+ const blame = this.showBlame === true ? '1' : '0';
+ if (this.$route?.query?.blame === blame) return;
+ this.$router.push({ path: this.$route.path, query: { ...this.$route.query, blame } });
+ },
},
};
</script>
@@ -299,19 +311,21 @@ export default {
<div v-if="blobInfo && !isLoading" id="fileHolder" class="file-holder">
<blob-header
:blob="blobInfo"
- :hide-viewer-switcher="!hasRichViewer || isBinaryFileType || isUsingLfs"
+ :hide-viewer-switcher="isBinaryFileType || isUsingLfs"
:is-binary="isBinaryFileType"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
:show-fork-suggestion="showForkSuggestion"
+ :show-blame-toggle="isBlameEnabled"
:project-path="projectPath"
:project-id="projectId"
@viewer-changed="handleViewerChanged"
@copy="onCopy"
@edit="editBlob"
@error="displayError"
+ @blame="handleToggleBlame"
>
<template #actions>
<blob-button-group
@@ -354,6 +368,7 @@ export default {
v-else
:blob="blobInfo"
:chunks="chunks"
+ :show-blame="showBlame"
:project-path="projectPath"
:current-ref="currentRef"
class="blob-viewer"
diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
index b6e3cdbb7a3..b6674114a20 100644
--- a/app/assets/javascripts/repository/components/commit_info.vue
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, sprintf } 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';
@@ -26,6 +26,11 @@ export default {
type: Object,
required: true,
},
+ prevBlameLink: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
return { showDescription: false };
@@ -35,6 +40,9 @@ export default {
// Strip the newline at the beginning
return this.commit?.descriptionHtml?.replace(/^&#x000A;/, '');
},
+ avatarLinkAltText() {
+ return sprintf(__(`%{username}'s avatar`), { username: this.commit.authorName });
+ },
},
methods: {
toggleShowDescription() {
@@ -58,6 +66,7 @@ export default {
v-if="commit.author"
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
+ :img-alt="avatarLinkAltText"
:img-size="32"
class="gl-my-2 gl-mr-4"
/>
@@ -67,10 +76,8 @@ export default {
: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">
+ <div class="commit-detail flex-list gl-display-flex gl-flex-grow-1 gl-min-w-0">
+ <div class="commit-content gl-w-full gl-text-truncate" data-testid="commit-content">
<gl-link
v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
:href="commit.webPath"
@@ -112,5 +119,6 @@ export default {
<div class="gl-flex-grow-1"></div>
<slot></slot>
</div>
+ <div v-if="prevBlameLink" v-safe-html:[$options.safeHtmlConfig]="prevBlameLink"></div>
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 97171a3282b..079d4c522a8 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -273,7 +273,7 @@ export default {
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
name="commit_message"
- data-qa-selector="commit_message_field"
+ data-testid="commit-message-field"
:state="form.fields['commit_message'].state"
:disabled="loading"
required
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 05d4d9e1f81..7f7a76cd4aa 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@git
import SafeHtml from '~/vue_shared/directives/safe_html';
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 CiIcon from '~/vue_shared/components/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import getRefMixin from '../mixins/get_ref';
@@ -17,7 +17,7 @@ export default {
CommitInfo,
ClipboardButton,
SignatureBadge,
- CiBadgeLink,
+ CiIcon,
GlButtonGroup,
GlButton,
GlLoadingIcon,
@@ -50,9 +50,6 @@ export default {
pipeline: pipelines?.length && pipelines[0].node,
};
},
- context: {
- isSingleRequest: true,
- },
error(error) {
throw error;
},
@@ -115,12 +112,10 @@ export default {
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
+ <div v-if="commit.pipeline" class="gl-ml-5">
+ <ci-icon
:status="commit.pipeline.detailedStatus"
- :details-path="commit.pipeline.detailedStatus.detailsPath"
:aria-label="statusTitle"
- :show-text="false"
class="js-commit-pipeline"
/>
</div>
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 526757e6147..6a81f11eb51 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -248,19 +248,19 @@ export default {
class="ml-1"
/>
</td>
- <td class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary">
+ <td class="d-none d-sm-table-cell tree-commit cursor-default">
<gl-link
v-if="commitData"
v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml"
:href="commitData.commitPath"
:title="commitData.message"
- class="str-truncated-100 tree-commit-link gl-text-secondary"
+ class="str-truncated-100 tree-commit-link gl-text-gray-600"
/>
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</gl-intersection-observer>
</td>
- <td class="tree-time-ago text-right cursor-default gl-text-secondary">
+ <td class="tree-time-ago text-right cursor-default gl-text-gray-600">
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
</gl-intersection-observer>
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 2ff138cabe5..86a5f5107f8 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,12 +1,12 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import {
SCOPE_ISSUES,
@@ -16,6 +16,7 @@ import {
SCOPE_NOTES,
SCOPE_COMMITS,
SCOPE_MILESTONES,
+ SCOPE_WIKI_BLOBS,
SEARCH_TYPE_ADVANCED,
} from '../constants';
import IssuesFilters from './issues_filters.vue';
@@ -25,6 +26,7 @@ import ProjectsFilters from './projects_filters.vue';
import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
+import WikiBlobsFilters from './wiki_blobs_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -34,6 +36,7 @@ export default {
BlobsFilters,
ProjectsFilters,
NotesFilters,
+ WikiBlobsFilters,
ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
@@ -60,20 +63,18 @@ export default {
return this.currentScope === SCOPE_PROJECTS;
},
showNotesFilters() {
- // 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;
+ return this.currentScope === SCOPE_NOTES;
},
showCommitsFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in commits scope
- return (
- this.currentScope === SCOPE_COMMITS && this.glFeatures.searchCommitsHideArchivedProjects
- );
+ return this.currentScope === SCOPE_COMMITS;
},
showMilestonesFilters() {
- // for now, the feature flag is placed here. Since we have only one filter in milestones scope
+ return this.currentScope === SCOPE_MILESTONES;
+ },
+ showWikiBlobsFilters() {
return (
- this.currentScope === SCOPE_MILESTONES &&
- this.glFeatures.searchMilestonesHideArchivedProjects
+ this.currentScope === SCOPE_WIKI_BLOBS &&
+ this.glFeatures?.searchProjectWikisHideArchivedProjects
);
},
showScopeNavigation() {
@@ -103,6 +104,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</sidebar-portal>
</section>
@@ -119,6 +121,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</div>
<small-screen-drawer-navigation class="gl-lg-display-none">
<scope-legacy-navigation />
@@ -129,6 +132,7 @@ export default {
<notes-filters v-if="showNotesFilters" />
<commits-filters v-if="showCommitsFilters" />
<milestones-filters v-if="showMilestonesFilters" />
+ <wiki-blobs-filters v-if="showWikiBlobsFilters" />
</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 ed90e2aaded..96a6f119da2 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -5,7 +5,16 @@ const checkboxLabel = s__('GlobalSearch|Include archived');
export const TRACKING_NAMESPACE = 'search:archived:select';
export const TRACKING_LABEL_CHECKBOX = 'checkbox';
-const scopes = ['projects', 'issues', 'merge_requests', 'notes', 'blobs', 'commits', 'milestones'];
+const scopes = [
+ 'projects',
+ 'issues',
+ 'merge_requests',
+ 'notes',
+ 'blobs',
+ 'commits',
+ 'milestones',
+ 'wiki_blobs',
+];
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
index ac36ae6b366..0ed2c24efba 100644
--- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
@@ -18,11 +18,8 @@ export default {
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation', 'searchType']),
- showArchivedFilter() {
- return this.glFeatures.searchBlobsHideArchivedProjects;
- },
showDivider() {
- return !this.useSidebarNavigation && this.showArchivedFilter;
+ return !this.useSidebarNavigation;
},
hrClasses() {
return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
@@ -35,6 +32,6 @@ export default {
<filters-template>
<language-filter class="gl-mb-5" />
<hr v-if="showDivider" :class="hrClasses" />
- <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
+ <archived-filter class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 4a2d3df6921..a77fb34cdba 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -41,10 +41,7 @@ export default {
);
},
showArchivedFilter() {
- return (
- archivedFilterData.scopes.includes(this.currentScope) &&
- this.glFeatures.searchIssuesHideArchivedProjects
- );
+ return archivedFilterData.scopes.includes(this.currentScope);
},
showDivider() {
return !this.useSidebarNavigation;
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 ebd0406bcec..97583730958 100644
--- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -55,12 +55,15 @@ export default {
},
i18n: I18N,
computed: {
- ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'aggregations']),
+ ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'urlQuery', 'aggregations']),
...mapGetters([
'filteredLabels',
'filteredUnselectedLabels',
'filteredAppliedSelectedLabels',
'appliedSelectedLabels',
+ 'unselectedLabels',
+ 'unappliedNewLabels',
+ 'labelAggregationBuckets',
]),
searchInputDescribeBy() {
if (this.isLoggedIn) {
@@ -100,10 +103,10 @@ export default {
return FIRST_DROPDOWN_INDEX;
},
hasSelectedLabels() {
- return this.filteredAppliedSelectedLabels.length > 0;
+ return this.filteredAppliedSelectedLabels?.length > 0;
},
hasUnselectedLabels() {
- return this.filteredUnselectedLabels.length > 0;
+ return this.filteredUnselectedLabels?.length > 0;
},
labelSearchBox() {
return this.$refs.searchLabelInputBox?.$el.querySelector('[role=searchbox]');
@@ -122,25 +125,30 @@ export default {
this.setLabelFilterSearch({ value });
},
},
- selectedFilters: {
+ selectedLabels: {
get() {
return this.combinedSelectedFilters;
},
set(value) {
this.setQuery({ key: this.$options.labelFilterData?.filterParam, value });
-
trackSelectCheckbox(value);
},
},
},
async created() {
- await this.fetchAllAggregation();
+ if (this.urlQuery?.[labelFilterData.filterParam]?.length > 0) {
+ await this.fetchAllAggregation();
+ }
},
methods: {
...mapActions(['fetchAllAggregation', 'setQuery', 'closeLabel', 'setLabelFilterSearch']),
- openDropdown() {
+ async openDropdown() {
this.isFocused = true;
+ if (!this.aggregations.error && this.filteredLabels?.length === 0) {
+ await this.fetchAllAggregation();
+ }
+
trackOpenDropdown();
},
closeDropdown(event) {
@@ -158,16 +166,8 @@ export default {
const { key } = event.target.closest('.gl-label').dataset;
this.closeLabel({ key });
},
- reactiveLabelColor(label) {
- const { color, key } = label;
-
- return this.query?.labels?.some((labelKey) => labelKey === key)
- ? color
- : `rgba(${rgbFromHex(color)}, 0.3)`;
- },
- isLabelClosable(label) {
- const { key } = label;
- return this.query?.labels?.some((labelKey) => labelKey === key);
+ inactiveLabelColor(label) {
+ return `rgba(${rgbFromHex(label.color)}, 0.3)`;
},
},
FIRST_DROPDOWN_INDEX,
@@ -188,13 +188,34 @@ export default {
</h5>
<div class="gl-my-5">
<gl-label
+ v-for="label in unappliedNewLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="inactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="false"
+ data-testid="unapplied-label"
+ />
+ <gl-label
+ v-for="label in unselectedLabels"
+ :key="label.key"
+ class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
+ :data-key="label.key"
+ :background-color="inactiveLabelColor(label)"
+ :title="label.title"
+ :show-close-button="false"
+ data-testid="unselected-label"
+ />
+ <gl-label
v-for="label in appliedSelectedLabels"
:key="label.key"
class="gl-mr-2 gl-mb-2 gl-bg-gray-10"
:data-key="label.key"
- :background-color="reactiveLabelColor(label)"
+ :background-color="label.color"
:title="label.title"
- :show-close-button="isLabelClosable(label)"
+ :show-close-button="true"
+ data-testid="label"
@close="onLabelClose"
/>
</div>
@@ -245,7 +266,7 @@ export default {
$options.i18n.DROPDOWN_HEADER
}}</gl-dropdown-section-header>
<gl-dropdown-form>
- <gl-form-checkbox-group v-model="selectedFilters">
+ <gl-form-checkbox-group v-model="selectedLabels">
<label-dropdown-items
v-if="hasSelectedLabels"
:labels="filteredAppliedSelectedLabels"
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 6e476ef7935..f86906ebd26 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -1,7 +1,6 @@
<script>
// 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 } from '../constants';
import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
@@ -16,15 +15,11 @@ export default {
FiltersTemplate,
ArchivedFilter,
},
- mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
...mapState(['useSidebarNavigation', 'searchType']),
showArchivedFilter() {
- return (
- archivedFilterData.scopes.includes(this.currentScope) &&
- this.glFeatures.searchMergeRequestsHideArchivedProjects
- );
+ return archivedFilterData.scopes.includes(this.currentScope);
},
showStatusFilter() {
return Object.values(statusFilterData.scopes).includes(this.currentScope);
diff --git a/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
new file mode 100644
index 00000000000..b1f386d9f4f
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/wiki_blobs_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'WikiBlobsFilters',
+ 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 b5446ecbb42..1559155a941 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -5,6 +5,8 @@ export const SCOPE_PROJECTS = 'projects';
export const SCOPE_NOTES = 'notes';
export const SCOPE_COMMITS = 'commits';
export const SCOPE_MILESTONES = 'milestones';
+export const SCOPE_WIKI_BLOBS = 'wiki_blobs';
+
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
index d01fd884bad..de05e9b80b2 100644
--- a/app/assets/javascripts/search/store/getters.js
+++ b/app/assets/javascripts/search/store/getters.js
@@ -1,10 +1,24 @@
-import { findKey } from 'lodash';
+import { findKey, intersection } from 'lodash';
import { languageFilterData } from '~/search/sidebar/components/language_filter/data';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, ICON_MAP } from './constants';
+const queryLabelFilters = (state) => state?.query?.[labelFilterData.filterParam] || [];
+const urlQueryLabelFilters = (state) => state?.urlQuery?.[labelFilterData.filterParam] || [];
+
+const appliedSelectedLabelsKeys = (state) =>
+ intersection(urlQueryLabelFilters(state), queryLabelFilters(state));
+
+const unselectedLabelsKeys = (state) =>
+ urlQueryLabelFilters(state)?.filter((label) => !queryLabelFilters(state)?.includes(label));
+
+const unappliedNewLabelKeys = (state) =>
+ state?.query?.labels?.filter((label) => !urlQueryLabelFilters(state)?.includes(label));
+
+export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
+
export const frequentGroups = (state) => {
return state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
};
@@ -39,25 +53,28 @@ export const filteredLabels = (state) => {
};
export const filteredAppliedSelectedLabels = (state) =>
- filteredLabels(state)?.filter((label) => state?.urlQuery?.labels?.includes(label.key));
+ filteredLabels(state)?.filter((label) => urlQueryLabelFilters(state)?.includes(label.key));
export const appliedSelectedLabels = (state) => {
return labelAggregationBuckets(state)?.filter((label) =>
- state?.urlQuery?.labels?.includes(label.key),
+ appliedSelectedLabelsKeys(state)?.includes(label.key),
);
};
-export const filteredUnselectedLabels = (state) => {
- if (!state?.urlQuery?.labels) {
- return filteredLabels(state);
- }
+export const filteredUnselectedLabels = (state) =>
+ filteredLabels(state)?.filter((label) => !urlQueryLabelFilters(state)?.includes(label.key));
- return filteredLabels(state)?.filter((label) => !state?.urlQuery?.labels?.includes(label.key));
-};
+export const unselectedLabels = (state) =>
+ labelAggregationBuckets(state).filter((label) =>
+ unselectedLabelsKeys(state)?.includes(label.key),
+ );
-export const currentScope = (state) => findKey(state.navigation, { active: true });
+export const unappliedNewLabels = (state) =>
+ labelAggregationBuckets(state).filter((label) =>
+ unappliedNewLabelKeys(state)?.includes(label.key),
+ );
-export const queryLanguageFilters = (state) => state.query[languageFilterData.filterParam] || [];
+export const currentScope = (state) => findKey(state.navigation, { active: true });
export const navigationItems = (state) =>
Object.values(state.navigation).map((item) => ({
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index f5f88e12163..d424ec6dfeb 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -9,7 +9,7 @@ import {
GlSkeletonLoader,
GlIcon,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { __, s__ } from '~/locale';
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
index 6f32c8c4165..722741b50e4 100644
--- a/app/assets/javascripts/sentry/init_sentry.js
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -23,7 +23,7 @@ const initSentry = () => {
const client = new BrowserClient({
// Sentry.init(...) options
dsn: gon.sentry_dsn,
- release: gon.version,
+ release: gon.revision,
allowUrls:
process.env.NODE_ENV === 'production'
? [gon.gitlab_url]
@@ -56,7 +56,7 @@ const initSentry = () => {
hub.bindClient(client);
hub.setTags({
- revision: gon.revision,
+ version: gon.version,
feature_category: gon.feature_category,
page,
});
@@ -75,7 +75,7 @@ const initSentry = () => {
// The _Sentry object is globally exported so it can be used by
// ./sentry_browser_wrapper.js
- // This hack allows us to load a single version of `@sentry/browser`
+ // This hack allows us to load a single version of `~/sentry/sentry_browser_wrapper`
// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/sentry/legacy_index.js b/app/assets/javascripts/sentry/legacy_index.js
index 604b982e128..688f8eb0a44 100644
--- a/app/assets/javascripts/sentry/legacy_index.js
+++ b/app/assets/javascripts/sentry/legacy_index.js
@@ -25,7 +25,7 @@ index();
// The _Sentry object is globally exported so it can be used by
// ./sentry_browser_wrapper.js
-// This hack allows us to load a single version of `@sentry/browser`
+// This hack allows us to load a single version of `~/sentry/sentry_browser_wrapper`
// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
// eslint-disable-next-line no-underscore-dangle
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index 03cf53fabef..99f5adf8e89 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -1,15 +1,23 @@
+/* eslint-disable no-console */
+
// The _Sentry object is globally exported so it can be used here
// This hack allows us to load a single version of `@sentry/browser`
-// in the browser (or none). See app/views/layouts/_head.html.haml
-// to find how it is imported.
+// in the browser (or none).
+
+// See app/views/layouts/_head.html.haml to find how it is imported.
-// This module wraps methods used by our production code.
-// Each export is names as we cannot export the entire namespace from *.
+// This module exports Sentry methods used by our production code.
/** @type {import('@sentry/core').captureException} */
export const captureException = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
+ // When Sentry is not configured during development, show console error
+ if (process.env.NODE_ENV === 'development' && !Sentry) {
+ console.error('[Sentry stub]', 'captureException(...) called with:', { ...args });
+ return;
+ }
+
Sentry?.captureException(...args);
};
diff --git a/app/assets/javascripts/service_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
deleted file mode 100644
index 7d6e7e81f3b..00000000000
--- a/app/assets/javascripts/service_ping_consent.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import $ from 'jquery';
-import { createAlert } from '~/alert';
-import axios from './lib/utils/axios_utils';
-import { parseBoolean } from './lib/utils/common_utils';
-import { __ } from './locale';
-
-export default () => {
- $('body').on('click', '.js-service-ping-consent-action', (e) => {
- e.preventDefault();
- e.stopImmediatePropagation(); // overwrite rails listener
-
- const { url, checkEnabled, servicePingEnabled } = e.target.dataset;
- const data = {
- application_setting: {
- version_check_enabled: parseBoolean(checkEnabled),
- service_ping_enabled: parseBoolean(servicePingEnabled),
- },
- };
-
- const hideConsentMessage = () =>
- document.querySelector('.service-ping-consent-message .js-close')?.click();
-
- axios
- .put(url, data)
- .then(() => {
- hideConsentMessage();
- })
- .catch(() => {
- hideConsentMessage();
- createAlert({
- message: __('Something went wrong. Try again later.'),
- });
- });
- });
-};
diff --git a/app/assets/javascripts/sessions/new/components/update_email.vue b/app/assets/javascripts/sessions/new/components/update_email.vue
index 124cd671169..f9b9a063808 100644
--- a/app/assets/javascripts/sessions/new/components/update_email.vue
+++ b/app/assets/javascripts/sessions/new/components/update_email.vue
@@ -1,6 +1,7 @@
<script>
import { GlForm, GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
+import { isUserEmail } from '~/lib/utils/forms';
import axios from '~/lib/utils/axios_utils';
import {
I18N_EMAIL,
@@ -10,7 +11,6 @@ import {
I18N_EMAIL_INVALID,
I18N_UPDATE_EMAIL_SUCCESS,
I18N_GENERIC_ERROR,
- EMAIL_REGEXP,
SUCCESS_RESPONSE,
FAILURE_RESPONSE,
} from '../constants';
@@ -48,7 +48,7 @@ export default {
return '';
}
- if (!EMAIL_REGEXP.test(this.email)) {
+ if (!isUserEmail(this.email)) {
return I18N_EMAIL_INVALID;
}
diff --git a/app/assets/javascripts/sessions/new/constants.js b/app/assets/javascripts/sessions/new/constants.js
index e9bd26099aa..eb2bc25d958 100644
--- a/app/assets/javascripts/sessions/new/constants.js
+++ b/app/assets/javascripts/sessions/new/constants.js
@@ -25,6 +25,5 @@ export const I18N_UPDATE_EMAIL_SUCCESS = s__(
);
export const VERIFICATION_CODE_REGEX = /^\d{6}$/;
-export const EMAIL_REGEXP = /^[^@\s]+@[^@\s]+$/; // Taken from DeviseEmailValidator
export const SUCCESS_RESPONSE = 'success';
export const FAILURE_RESPONSE = 'failure';
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 609a9355d20..745122afb4a 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -47,7 +47,6 @@ export default {
class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-test-id="edit-link"
- data-qa-selector="edit_link"
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 9d6a8bf47e0..55c5b04dbe3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -58,7 +58,6 @@ export default {
type="button"
class="gl-button btn-link gl-reset-color!"
data-testid="assign-yourself"
- data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
{{ __('assign yourself') }}
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
index 5ca18969f0b..06ac2cb715d 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
@@ -58,7 +58,6 @@ export default {
<template v-for="label in sortedSelectedLabels" v-else>
<gl-label
:key="label.id"
- data-qa-selector="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/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 377200ab804..3d9a5893c67 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
@@ -81,7 +81,6 @@ export default {
:value="searchKey"
:placeholder="__('Search labels')"
:disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="$emit('input', $event)"
@keydown.enter="$emit('searchEnter', $event)"
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index c9e651370f9..1497b229a59 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -27,11 +27,10 @@ export default {
<gl-sprintf
:message="
__(
- 'Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment.',
+ 'Unlock this discussion? %{strongStart}Everyone%{strongEnd} will be able to comment.',
)
"
>
- <template #issuableDisplayName>{{ issuableDisplayName }}</template>
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
@@ -42,11 +41,10 @@ export default {
<gl-sprintf
:message="
__(
- 'Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment.',
+ 'Lock this discussion? Only %{strongStart}project members%{strongEnd} will be able to comment.',
)
"
>
- <template #issuableDisplayName>{{ issuableDisplayName }}</template>
<template #strong="{ content }"
><strong>{{ content }}</strong></template
>
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 165499696de..977d1d6f668 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -50,12 +50,12 @@ export default {
issueCapitalized: __('Issue'),
mergeRequest: __('merge request'),
mergeRequestCapitalized: __('Merge request'),
- lockingMergeRequest: __('Locking %{issuableDisplayName}'),
- unlockingMergeRequest: __('Unlocking %{issuableDisplayName}'),
- lockMergeRequest: __('Lock %{issuableDisplayName}'),
- unlockMergeRequest: __('Unlock %{issuableDisplayName}'),
- lockedMessage: __('%{issuableDisplayName} locked.'),
- unlockedMessage: __('%{issuableDisplayName} unlocked.'),
+ lockingMergeRequest: __('Locking discussion'),
+ unlockingMergeRequest: __('Unlocking discussion'),
+ lockMergeRequest: __('Lock discussion'),
+ unlockMergeRequest: __('Unlock discussion'),
+ lockedMessage: __('Discussion locked.'),
+ unlockedMessage: __('Discussion unlocked.'),
},
data() {
return {
@@ -152,7 +152,7 @@ export default {
})
.catch(() => {
const alertMessage = __(
- 'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
+ 'Something went wrong trying to change the locked state of the discussion',
);
createAlert({
message: sprintf(alertMessage, { issuableDisplayName: this.issuableDisplayName }),
@@ -170,9 +170,14 @@ export default {
</script>
<template>
- <li v-if="isMovedMrSidebar && isIssuable" class="gl-dropdown-item">
- <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked">
- <span class="gl-dropdown-item-text-wrapper">
+ <li v-if="isMovedMrSidebar && isIssuable" class="gl-new-dropdown-item">
+ <button
+ type="button"
+ class="gl-new-dropdown-item-content"
+ data-testid="issuable-lock"
+ @click="toggleLocked"
+ >
+ <span class="gl-new-dropdown-item-text-wrapper">
<template v-if="isLoading">
<gl-loading-icon inline size="sm" /> {{ lockToggleInProgressText }}
</template>
diff --git a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
index 34a4da946d6..ea8e0c4b950 100644
--- a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
@@ -1,26 +1,20 @@
<script>
import {
GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
GlButton,
+ GlCollapsibleListbox,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-
+import { debounce } from 'lodash';
+import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import axios from '~/lib/utils/axios_utils';
export default {
components: {
GlIcon,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownForm,
- GlDropdownItem,
- GlSearchBoxByType,
GlButton,
+ GlCollapsibleListbox,
},
directives: {
GlTooltip,
@@ -51,82 +45,58 @@ export default {
},
data() {
return {
- projectsListLoading: false,
- projectsListLoadFailed: false,
- searchKey: '',
projects: [],
- selectedProject: null,
- projectItemClick: false,
+ projectsList: [],
+ selectedProjects: [],
+ noResultsText: '',
+ isSearching: false,
};
},
- computed: {
- hasNoSearchResults() {
- return Boolean(
- !this.projectsListLoading &&
- !this.projectsListLoadFailed &&
- this.searchKey &&
- !this.projects.length,
- );
- },
- failedToLoadResults() {
- return !this.projectsListLoading && this.projectsListLoadFailed;
- },
- },
- watch: {
- searchKey(value = '') {
- this.fetchProjects(value);
- },
+ mounted() {
+ this.fetchProjects = debounce(this.fetchProjects, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
- fetchProjects(search = '') {
- this.projectsListLoading = true;
- this.projectsListLoadFailed = false;
- return axios
- .get(this.projectsFetchPath, {
+ triggerSearch() {
+ this.$refs.dropdown.search();
+ },
+ async fetchProjects(search = '') {
+ this.isSearching = true;
+
+ try {
+ const { data } = await axios.get(this.projectsFetchPath, {
params: {
search,
},
- })
- .then(({ data }) => {
- this.projects = data;
- this.$refs.searchInput.focusInput();
- })
- .catch(() => {
- this.projectsListLoadFailed = true;
- })
- .finally(() => {
- this.projectsListLoading = false;
});
- },
- isSelectedProject(project) {
- if (this.selectedProject) {
- return this.selectedProject.id === project.id;
- }
- return false;
- },
- /**
- * This handler is to prevent dropdown
- * from closing when an item is selected
- * and emit an event only when dropdown closes.
- */
- handleDropdownHide(e) {
- if (this.projectItemClick) {
- e.preventDefault();
- this.projectItemClick = false;
- } else {
- this.$emit('dropdown-close');
+ this.projects = data;
+ this.projectsList = data.map((item) => ({
+ value: item.id,
+ text: item.name_with_namespace,
+ }));
+
+ if (!this.projectsList.length) {
+ this.noResultsText = __('No matching results');
+ }
+ } catch (e) {
+ this.noResultsText = __('Failed to load projects');
+ } finally {
+ this.isSearching = false;
}
},
- handleDropdownCloseClick() {
- this.$refs.dropdown.hide();
- },
- handleProjectSelect(project) {
- this.selectedProject = project.id === this.selectedProject?.id ? null : project;
- this.projectItemClick = true;
+ handleProjectSelect(items) {
+ // hack: simulate a single select to prevent the dropdown from closing
+ // todo: switch back to single select when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2363 is fixed
+ this.selectedProjects = [items[items.length - 1]];
},
handleMoveClick() {
- this.$refs.dropdown.hide();
- this.$emit('move-issuable', this.selectedProject);
+ this.$refs.dropdown.close();
+ this.$emit(
+ 'move-issuable',
+ this.projects.find((item) => item.id === this.selectedProjects[0]),
+ );
+ },
+ handleDropdownHide() {
+ this.$emit('dropdown-close');
},
},
};
@@ -143,79 +113,45 @@ export default {
>
<gl-icon name="arrow-right" />
</div>
- <gl-dropdown
+ <gl-collapsible-listbox
ref="dropdown"
+ v-model="selectedProjects"
+ :items="projectsList"
:block="true"
- :disabled="moveInProgress || disabled"
- class="hide-collapsed"
- toggle-class="js-sidebar-dropdown-toggle"
- @shown="fetchProjects"
- @hide="handleDropdownHide"
+ :multiple="true"
+ :searchable="true"
+ :searching="isSearching"
+ :search-placeholder="__('Search project')"
+ :no-results-text="noResultsText"
+ :header-text="dropdownButtonTitle"
+ @hidden="handleDropdownHide"
+ @shown="triggerSearch"
+ @search="fetchProjects"
+ @select="handleProjectSelect"
>
- <template #button-content
- ><gl-loading-icon v-if="moveInProgress" size="sm" class="gl-mr-3" />{{
- dropdownButtonTitle
- }}</template
- >
- <gl-dropdown-form class="gl-pt-0">
- <div
- data-testid="header"
- class="gl-display-flex gl-pb-3 gl-border-1 gl-border-b-solid gl-border-gray-100"
- >
- <span class="gl-flex-grow-1 gl-text-center gl-font-weight-bold gl-py-1">{{
- dropdownHeaderTitle
- }}</span>
- <gl-button
- variant="link"
- icon="close"
- class="gl-mr-2 gl-w-auto! gl-p-2!"
- :aria-label="__('Close')"
- @click.prevent="handleDropdownCloseClick"
- />
- </div>
- <gl-search-box-by-type
- ref="searchInput"
- v-model.trim="searchKey"
- :placeholder="__('Search project')"
- :debounce="300"
- />
- <div data-testid="content" class="dropdown-content">
- <gl-loading-icon v-if="projectsListLoading" size="lg" class="gl-p-5" />
- <ul v-else>
- <gl-dropdown-item
- v-for="project in projects"
- :key="project.id"
- is-check-item
- :is-checked="isSelectedProject(project)"
- @click.stop.prevent="handleProjectSelect(project)"
- >{{ project.name_with_namespace }}</gl-dropdown-item
- >
- </ul>
- <div v-if="hasNoSearchResults" class="gl-text-center gl-p-3">
- {{ __('No matching results') }}
- </div>
- <div
- v-if="failedToLoadResults"
- data-testid="failed-load-results"
- class="gl-text-center gl-p-3"
- >
- {{ __('Failed to load projects') }}
- </div>
- </div>
- <div
- data-testid="footer"
- class="gl-pt-3 gl-px-3 gl-border-1 gl-border-t-solid gl-border-gray-100"
+ <template #toggle>
+ <gl-button
+ :loading="moveInProgress"
+ size="medium"
+ class="gl-w-full js-sidebar-dropdown-toggle hide-collapsed"
+ data-testid="dropdown-button"
+ :disabled="moveInProgress || disabled"
+ >{{ dropdownButtonTitle }}</gl-button
>
+ </template>
+ <template #footer>
+ <div data-testid="footer" class="gl-p-3">
<gl-button
category="primary"
variant="confirm"
- :disabled="!Boolean(selectedProject)"
- class="gl-w-full issuable-move-button"
+ :disabled="!Boolean(selectedProjects.length)"
+ class="gl-w-full"
+ data-testid="dropdown-move-button"
@click="handleMoveClick"
>{{ __('Move') }}</gl-button
>
</div>
- </gl-dropdown-form>
- </gl-dropdown>
+ </template>
+ </gl-collapsible-listbox>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
index b764d660d63..40893f10109 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -49,9 +49,6 @@ export default {
error,
});
},
- context: {
- isSingleRequest: true,
- },
},
},
computed: {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
index a7db3b3d09f..d8e61c135e7 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar.vue
@@ -40,7 +40,6 @@ export default {
:width="imgSize"
:class="`s${imgSize}`"
class="avatar avatar-inline m-0"
- data-qa-selector="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/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index ee9edd6a022..1bcbf2167e9 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -62,7 +62,11 @@ export default {
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
- <span v-if="hasNoUsers" class="no-value" data-testid="no-value">
+ <span
+ v-if="hasNoUsers"
+ class="no-value gl-display-flex gl-font-base gl-line-height-normal"
+ data-testid="no-value"
+ >
{{ __('None') }}
<template v-if="editable">
-
@@ -71,7 +75,6 @@ export default {
variant="link"
class="gl-ml-2"
data-testid="assign-yourself"
- data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
<span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span>
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
index e2a3efa096f..e14fee5bfb8 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity_widget.vue
@@ -112,7 +112,7 @@ export default {
</script>
<template>
- <div ref="sidebarSeverity" class="block">
+ <div ref="sidebarSeverity" class="block" data-testid="severity-block-container">
<sidebar-editable-item
ref="toggle"
:loading="isUpdating"
@@ -131,7 +131,7 @@ export default {
</gl-sprintf>
</gl-tooltip>
</div>
- <div class="hide-collapsed">
+ <div class="hide-collapsed" data-testid="incident-severity">
<severity-token :severity="selectedItem" />
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
index ba0bf783315..7ce1ceb4bb8 100644
--- a/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ToggleSidebar',
@@ -10,6 +11,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
collapsed: {
type: Boolean,
@@ -29,7 +31,13 @@ export default {
return this.collapsed ? 'chevron-double-lg-left' : 'chevron-double-lg-right';
},
allCssClasses() {
- return [this.cssClasses, { 'js-sidebar-collapsed': this.collapsed }];
+ return [
+ this.cssClasses,
+ {
+ 'js-sidebar-collapsed': this.collapsed,
+ 'gl-mt-2': this.glFeatures.notificationsTodosButtons,
+ },
+ ];
},
},
watch: {
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 4b6dbdcc2c9..12e60a9ed4e 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -799,8 +799,7 @@ export function mountAssigneesDropdown() {
});
}
-const isAssigneesWidgetShown =
- (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
+const isAssigneesWidgetShown = isInIssuePage() || isInDesignPage() || isInMRPage();
export function mountSidebar(mediator, store) {
mountSidebarTodoWidget();
diff --git a/app/assets/javascripts/sidebar/queries/constants.js b/app/assets/javascripts/sidebar/queries/constants.js
index 0844abc4599..6bcdc01a003 100644
--- a/app/assets/javascripts/sidebar/queries/constants.js
+++ b/app/assets/javascripts/sidebar/queries/constants.js
@@ -12,8 +12,8 @@ import {
WORKSPACE_PROJECT,
} from '~/issues/constants';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import abuseReportLabelsQuery from '~/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql';
-import createAbuseReportLabelMutation from '~/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql';
+import abuseReportLabelsQuery from '~/admin/abuse_report/graphql/abuse_report_labels.query.graphql';
+import createAbuseReportLabelMutation from '~/admin/abuse_report/graphql/create_abuse_report_label.mutation.graphql';
import createGroupOrProjectLabelMutation from '../components/labels/labels_select_widget/graphql/create_label.mutation.graphql';
import updateTestCaseLabelsMutation from '../components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
import epicLabelsQuery from '../components/labels/labels_select_widget/graphql/epic_labels.query.graphql';
diff --git a/app/assets/javascripts/silent_mode_settings/components/app.vue b/app/assets/javascripts/silent_mode_settings/components/app.vue
index 2dd0449448c..a151492c75c 100644
--- a/app/assets/javascripts/silent_mode_settings/components/app.vue
+++ b/app/assets/javascripts/silent_mode_settings/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlToggle, GlBadge } from '@gitlab/ui';
+import { GlToggle } from '@gitlab/ui';
import { updateApplicationSettings } from '~/rest_api';
import { createAlert } from '~/alert';
import toast from '~/vue_shared/plugins/global_toast';
@@ -13,11 +13,9 @@ export default {
saveError: s__('SilentMode|There was an error updating the Silent Mode Settings.'),
enabled: __('enabled'),
disabled: __('disabled'),
- experiment: __('Experiment'),
},
components: {
GlToggle,
- GlBadge,
},
props: {
isSilentModeEnabled: {
@@ -62,9 +60,5 @@ export default {
:label="$options.i18n.toggleLabel"
:is-loading="isLoading"
@change="updateSilentModeSettings"
- >
- <template #label
- >{{ $options.i18n.toggleLabel }} <gl-badge>{{ $options.i18n.experiment }}</gl-badge></template
- >
- </gl-toggle>
+ />
</template>
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 11896a75798..1e01da795e8 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -40,9 +40,12 @@ export default class SingleFileDiff {
this.$chevronDownIcon.removeClass('gl-display-none');
}
- $('.js-file-title, .click-to-expand', this.file).on('click', (e) => {
+ $('.js-file-title', this.file).on('click', (e) => {
this.toggleDiff($(e.target));
});
+ $('.click-to-expand', this.file).on('click', (e) => {
+ this.toggleDiff($(e.currentTarget));
+ });
}
toggleDiff($target, cb) {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 56ea931fc8c..573b8777ade 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -213,7 +213,7 @@ export default {
</script>
<template>
<div class="detail-page-header">
- <div class="detail-page-header-body">
+ <div class="detail-page-header-body gl-align-items-baseline">
<div
class="snippet-box has-tooltip d-flex align-items-center gl-mr-2 mb-1"
data-testid="snippet-container"
@@ -235,12 +235,20 @@ export default {
<template #author>
<a :href="snippet.author.webUrl" class="d-inline">
<gl-avatar :size="24" :src="snippet.author.avatarUrl" />
- <span class="bold">{{ snippet.author.name }}</span>
+ <span class="bold gl-display-none gl-sm-display-inline">{{
+ snippet.author.name
+ }}</span>
+ <strong
+ v-if="snippet.author.username"
+ data-testid="authored-username"
+ class="gl-display-inline gl-sm-display-none!"
+ >@{{ snippet.author.username }}</strong
+ >
</a>
<gl-emoji
v-if="snippet.author.status"
v-gl-tooltip
- class="gl-vertical-align-baseline font-size-inherit gl-mr-1"
+ class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1"
:title="snippet.author.status.message"
:data-name="snippet.author.status.emoji"
/>
@@ -249,7 +257,7 @@ export default {
</div>
</div>
- <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions">
+ <div v-if="hasPersonalSnippetActions" class="detail-page-header-actions gl-align-self-start">
<div class="d-none d-sm-flex">
<template v-for="(action, index) in personalSnippetActions">
<div
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index 279e689bd8d..e8410a51905 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -70,7 +70,6 @@ export default {
:toggle-text="$options.i18n.createNew"
:toggle-id="$options.toggleId"
:dropdown-offset="dropdownOffset"
- data-qa-selector="new_menu_toggle"
data-testid="new-menu-toggle"
@shown="dropdownOpen = true"
@hidden="dropdownOpen = false"
diff --git a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
index e73b9b275ee..414e4a54a8e 100644
--- a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
@@ -139,8 +139,8 @@ export default {
:key="item.id"
:item="item"
:is-flyout="true"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
</ul>
<svg
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
index b85b163cea9..1a681d6e9bd 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue
@@ -2,7 +2,7 @@
import { debounce } from 'lodash';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import Tracking from '~/tracking';
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 61fa360c41f..e6137bda401 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
@@ -15,7 +15,14 @@ import { truncate } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
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 {
+ ARROW_DOWN_KEY,
+ ARROW_UP_KEY,
+ END_KEY,
+ HOME_KEY,
+ ESC_KEY,
+ NUMPAD_ENTER_KEY,
+} from '~/lib/utils/keys';
import {
COMMAND_PALETTE,
MIN_SEARCH_TERM,
@@ -215,6 +222,8 @@ export default {
this.focusNextItem(event, elements, 1);
} else if (code === ESC_KEY) {
this.$refs.searchModal.close();
+ } else if (code === NUMPAD_ENTER_KEY) {
+ event.target?.firstChild.click();
} else {
stop = false;
}
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
index 9167be5c1cc..914d3c393f5 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue
@@ -1,5 +1,6 @@
<script>
import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { kebabCase } from 'lodash';
import { PLACES } from '~/vue_shared/global_search/constants';
import { TRACKING_UNKNOWN_ID, TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
@@ -20,7 +21,7 @@ export default {
group() {
return {
name: this.$options.i18n.PLACES,
- items: this.contextSwitcherLinks.map(({ title, link }) => ({
+ items: this.contextSwitcherLinks.map(({ title, link, ...rest }) => ({
text: title,
href: link,
extraAttrs: {
@@ -35,6 +36,12 @@ export default {
// QA attributes
'data-testid': 'places-item-link',
'data-qa-places-item': title,
+
+ // Any other data- attributes (e.g., for @rails/ujs)
+ ...Object.entries(rest).reduce((acc, [name, value]) => {
+ if (name.startsWith('data')) acc[kebabCase(name)] = value;
+ return acc;
+ }, {}),
},
})),
};
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index 91b781b8235..a672e254004 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -145,8 +145,8 @@ export default {
:items="item.items"
@mouseover="isMouseOverFlyout = true"
@mouseleave="isMouseOverFlyout = false"
- @pin-add="(itemId) => $emit('pin-add', itemId)"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
<gl-collapse
@@ -162,8 +162,8 @@ export default {
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)"
+ @pin-add="(itemId, itemTitle) => $emit('pin-add', itemId, itemTitle)"
+ @pin-remove="(itemId, itemTitle) => $emit('pin-remove', itemId, itemTitle)"
/>
</ul>
</slot>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 5416f86abeb..3ae33bf8b37 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -70,14 +70,16 @@ export default {
return {
isMouseIn: false,
canClickPinButton: false,
- pillCount: this.item.pill_count,
};
},
computed: {
+ pillData() {
+ return this.item.pill_count;
+ },
hasPill() {
return (
- Number.isFinite(this.pillCount) ||
- (typeof this.pillCount === 'string' && this.pillCount !== '')
+ Number.isFinite(this.pillData) ||
+ (typeof this.pillData === 'string' && this.pillData !== '')
);
},
isPinnable() {
@@ -188,12 +190,22 @@ export default {
eventHub.$off('updatePillValue', this.updatePillValue);
},
methods: {
+ pinAdd() {
+ this.$emit('pin-add', this.item.id, this.item.title);
+ },
+ pinRemove() {
+ this.$emit('pin-remove', this.item.id, this.item.title);
+ },
togglePointerEvents() {
this.canClickPinButton = this.isMouseIn;
},
updatePillValue({ value, itemId }) {
if (this.item.id === itemId) {
- this.pillCount = value;
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // fixing this linting issue is causing the pills not to async update
+ //
+ // eslint-disable-next-line vue/no-mutating-props
+ this.item.pill_count = value;
}
},
},
@@ -214,7 +226,6 @@ export default {
class="gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
:class="computedLinkClasses"
data-testid="nav-item-link"
- data-qa-selector="nav_item_link"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
@@ -258,7 +269,7 @@ export default {
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
>
- {{ pillCount }}
+ {{ pillData }}
</gl-badge>
</span>
</component>
@@ -273,7 +284,7 @@ export default {
data-testid="nav-item-unpin"
icon="thumbtack-solid"
size="small"
- @click="$emit('pin-remove', item.id)"
+ @click="pinRemove"
@transitionend="togglePointerEvents"
/>
<gl-button
@@ -286,7 +297,7 @@ export default {
data-testid="nav-item-pin"
icon="thumbtack"
size="small"
- @click="$emit('pin-add', item.id)"
+ @click="pinAdd"
@transitionend="togglePointerEvents"
/>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index ea3e9e9df1f..05040218164 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -84,8 +84,8 @@ export default {
return { ...i, title };
});
},
- onPinRemove(itemId) {
- this.$emit('pin-remove', itemId);
+ onPinRemove(itemId, itemTitle) {
+ this.$emit('pin-remove', itemId, itemTitle);
},
},
};
@@ -113,7 +113,7 @@ export default {
:key="item.id"
:item="item"
is-in-pinned-section
- @pin-remove="onPinRemove"
+ @pin-remove="onPinRemove(item.id, item.title)"
/>
</draggable>
<li v-else class="gl-text-secondary gl-font-sm gl-py-3" style="margin-left: 2.5rem">
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 772072c0996..c04addf5262 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -1,6 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlBreakpointInstance, breakpoints } from '@gitlab/ui/dist/utils';
+import { s__, sprintf } from '~/locale';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PANELS_WITH_PINS } from '../constants';
@@ -16,7 +17,10 @@ export default {
PinnedSection,
},
mixins: [glFeatureFlagsMixin()],
-
+ i18n: {
+ pinAdded: s__('Navigation|%{title} added to pinned items'),
+ pinRemoved: s__('Navigation|%{title} removed from pinned items'),
+ },
provide() {
return {
pinnedItemIds: this.changedPinnedItemIds,
@@ -111,12 +115,22 @@ export default {
window.removeEventListener('resize', this.decideFlyoutState);
},
methods: {
- createPin(itemId) {
+ createPin(itemId, itemTitle) {
this.changedPinnedItemIds.ids.push(itemId);
+ this.$toast.show(
+ sprintf(this.$options.i18n.pinAdded, {
+ title: itemTitle,
+ }),
+ );
this.updatePins();
},
- destroyPin(itemId) {
+ destroyPin(itemId, itemTitle) {
this.changedPinnedItemIds.ids = this.changedPinnedItemIds.ids.filter((id) => id !== itemId);
+ this.$toast.show(
+ sprintf(this.$options.i18n.pinRemoved, {
+ title: itemTitle,
+ }),
+ );
this.updatePins();
},
movePin(fromId, toId, isDownwards) {
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 88ea4d828b7..3c47245a1a6 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -115,6 +115,7 @@ export default {
<gl-badge
v-if="sidebarData.gitlab_com_and_canary"
variant="success"
+ data-testid="canary-badge-link"
:href="sidebarData.canary_toggle_com_url"
size="sm"
>
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 891e883b6c0..5712b716f48 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -8,7 +8,6 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __, sprintf } from '~/locale';
-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';
@@ -39,14 +38,13 @@ export default {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlButton,
- NewNavToggle,
UserMenuProfileItem,
},
directives: {
SafeHtml,
},
mixins: [Tracking.mixin()],
- inject: ['toggleNewNavEndpoint', 'isImpersonating'],
+ inject: ['isImpersonating'],
props: {
data: {
required: true,
@@ -301,13 +299,6 @@ export default {
/>
</gl-disclosure-dropdown-group>
- <gl-disclosure-dropdown-group bordered>
- <template #group-label>
- <span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
- </template>
- <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
- </gl-disclosure-dropdown-group>
-
<gl-disclosure-dropdown-group
v-if="data.can_sign_out"
bordered
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index f9e488ea5ee..9e540175b48 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { initStatusTriggers } from '../header';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
@@ -10,6 +11,8 @@ import {
import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
+Vue.use(GlToast);
+
const getTrialStatusWidgetData = (sidebarData) => {
if (sidebarData.trial_status_widget_data_attrs && sidebarData.trial_status_popover_data_attrs) {
const {
@@ -63,13 +66,7 @@ export const initSuperSidebar = () => {
if (!el) return false;
- const {
- rootPath,
- sidebar,
- toggleNewNavEndpoint,
- forceDesktopExpandedSidebar,
- commandPalette,
- } = el.dataset;
+ const { rootPath, sidebar, forceDesktopExpandedSidebar, commandPalette } = el.dataset;
bindSuperSidebarCollapsedEvents(forceDesktopExpandedSidebar);
initSuperSidebarCollapsedState(parseBoolean(forceDesktopExpandedSidebar));
@@ -95,7 +92,6 @@ export const initSuperSidebar = () => {
name: 'SuperSidebarRoot',
provide: {
rootPath,
- toggleNewNavEndpoint,
isImpersonating,
...getTrialStatusWidgetData(sidebarData),
commandPaletteCommands,
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index d2fb72adb85..3d6eef62ad2 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/tags/components/delete_tag_modal.vue b/app/assets/javascripts/tags/components/delete_tag_modal.vue
index c4f9db70d2a..9a0cc026223 100644
--- a/app/assets/javascripts/tags/components/delete_tag_modal.vue
+++ b/app/assets/javascripts/tags/components/delete_tag_modal.vue
@@ -151,7 +151,6 @@ export default {
ref="deleteTagButton"
:disabled="deleteButtonDisabled"
variant="danger"
- data-qa-selector="delete_tag_confirmation_button"
data-testid="delete-tag-confirmation-button"
@click="submitForm"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
index 74c41700f43..7962c8573df 100644
--- a/app/assets/javascripts/terraform/components/init_command_modal.vue
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -40,15 +40,14 @@ export default {
},
methods: {
getModalInfoCopyStr() {
- const stateNameEncoded = this.stateName
- ? encodeURIComponent(this.stateName)
- : '<YOUR-STATE-NAME>';
+ const stateNameEncoded = this.stateName ? encodeURIComponent(this.stateName) : 'default';
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
+export TF_STATE_NAME=${stateNameEncoded}
terraform init \\
- -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
- -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
- -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="address=${this.terraformApiUrl}/$TF_STATE_NAME" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/$TF_STATE_NAME/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index c88c528a632..273cd599308 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -11,14 +11,14 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, sprintf } from '~/locale';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import StateActions from './states_table_actions.vue';
export default {
components: {
- CiBadgeLink,
+ CiIcon,
GlAlert,
GlBadge,
GlLink,
@@ -198,10 +198,10 @@ export default {
:id="`terraformJobStatusContainer${item.name}`"
class="gl-my-2"
>
- <ci-badge-link
+ <ci-icon
:id="`terraformJobStatus${item.name}`"
:status="pipelineDetailedStatus(item)"
- class="gl-py-1"
+ show-status-text
/>
<gl-tooltip
diff --git a/app/assets/javascripts/time_tracking/components/timelogs_app.vue b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
index 7bb9b6c52a5..8464384ac7c 100644
--- a/app/assets/javascripts/time_tracking/components/timelogs_app.vue
+++ b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
@@ -1,5 +1,4 @@
<script>
-import * as Sentry from '@sentry/browser';
import {
GlButton,
GlFormGroup,
@@ -8,6 +7,7 @@ import {
GlKeysetPagination,
GlDatepicker,
} from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert } from '~/alert';
import { formatTimeSpent } from '~/lib/utils/datetime_utility';
import { s__ } from '~/locale';
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 7e55f56279e..345db1752f6 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -46,12 +46,6 @@ export default {
columnClass: 'gl-w-40p',
},
{
- key: 'namespace',
- label: __('Namespace'),
- thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
- },
- {
key: 'actions',
label: '',
tdClass: 'gl-text-right',
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 43aa9b94b3a..846b0d1791f 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -54,12 +54,6 @@ export default {
columnClass: 'gl-w-40p',
},
{
- key: 'namespace',
- label: __('Namespace'),
- thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
- },
- {
key: 'actions',
label: '',
tdClass: 'gl-text-right',
diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue
index ee88b4ec339..4245b39dec1 100644
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -29,9 +29,6 @@ export default {
removeProject(project) {
this.$emit('removeProject', project);
},
- namespaceFallback(namespace) {
- return namespace?.fullPath || '';
- },
},
};
</script>
@@ -50,13 +47,7 @@ export default {
</template>
<template #cell(project)="{ item }">
- <span data-testid="token-access-project-name">{{ item.name }}</span>
- </template>
-
- <template #cell(namespace)="{ item }">
- <span data-testid="token-access-project-namespace">
- {{ namespaceFallback(item.namespace) }}
- </span>
+ <span data-testid="token-access-project-name">{{ item.fullPath }}</span>
</template>
<template #cell(actions)="{ item }">
diff --git a/app/assets/javascripts/token_access/graphql/cache_config.js b/app/assets/javascripts/token_access/graphql/cache_config.js
new file mode 100644
index 00000000000..2db534b7eb5
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/cache_config.js
@@ -0,0 +1,14 @@
+export default {
+ typePolicies: {
+ Project: {
+ fields: {
+ ciCdSettings: {
+ merge: true,
+ },
+ ciJobTokenScope: {
+ merge: true,
+ },
+ },
+ },
+ },
+};
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
index 9258d5eba45..45bd1921dbd 100644
--- a/app/assets/javascripts/token_access/index.js
+++ b/app/assets/javascripts/token_access/index.js
@@ -2,11 +2,12 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import TokenAccessApp from './components/token_access_app.vue';
+import cacheConfig from './graphql/cache_config';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { cacheConfig }),
});
export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 46278152879..bc416b20e80 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -1,5 +1,7 @@
export const SNOWPLOW_JS_SOURCE = 'gitlab-javascript';
+export const MAX_LOCAL_STORAGE_QUEUE_SIZE = 100;
+
export const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
hostname: window.location.hostname,
@@ -15,6 +17,7 @@ export const DEFAULT_SNOWPLOW_OPTIONS = {
forms: { allow: [] },
fields: { allow: [] },
},
+ maxLocalStorageQueueSize: MAX_LOCAL_STORAGE_QUEUE_SIZE,
};
export const ACTION_ATTR_SELECTOR = '[data-track-action]';
diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 99e4a6aa3c7..91512292eb6 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import getStandardContext from './get_standard_context';
export function dispatchSnowplowEvent(
diff --git a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
index 5dfa9c67852..f994cad6881 100644
--- a/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
+++ b/app/assets/javascripts/users/profile/actions/components/user_actions_app.vue
@@ -83,7 +83,13 @@ export default {
<template>
<span>
- <gl-disclosure-dropdown icon="ellipsis_v" category="tertiary" no-caret :items="dropdownItems" />
+ <gl-disclosure-dropdown
+ data-testid="user-profile-actions"
+ icon="ellipsis_v"
+ category="tertiary"
+ no-caret
+ :items="dropdownItems"
+ />
<abuse-category-selector
v-if="reportedUserId"
:reported-user-id="reportedUserId"
diff --git a/app/assets/javascripts/users/profile/components/report_abuse_button.vue b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
deleted file mode 100644
index 0e41a214888..00000000000
--- a/app/assets/javascripts/users/profile/components/report_abuse_button.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-
-import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-
-export default {
- name: 'ReportAbuseButton',
- components: {
- GlButton,
- AbuseCategorySelector,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: ['reportedUserId', 'reportedFromUrl'],
- i18n: {
- reportAbuse: s__('ReportAbuse|Report abuse to administrator'),
- },
- data() {
- return {
- open: false,
- };
- },
- computed: {
- buttonTooltipText() {
- return this.$options.i18n.reportAbuse;
- },
- },
- methods: {
- toggleDrawer(open) {
- this.open = open;
- },
- hideTooltips() {
- this.$root.$emit(BV_HIDE_TOOLTIP);
- },
- },
-};
-</script>
-<template>
- <span>
- <gl-button
- v-gl-tooltip="buttonTooltipText"
- category="primary"
- :aria-label="buttonTooltipText"
- icon="error"
- @click="toggleDrawer(true)"
- @mouseout="hideTooltips"
- />
- <abuse-category-selector
- :reported-user-id="reportedUserId"
- :reported-from-url="reportedFromUrl"
- :show-drawer="open"
- @close-drawer="toggleDrawer(false)"
- />
- </span>
-</template>
diff --git a/app/assets/javascripts/users/profile/index.js b/app/assets/javascripts/users/profile/index.js
deleted file mode 100644
index 3ae3cc2de98..00000000000
--- a/app/assets/javascripts/users/profile/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import ReportAbuseButton from './components/report_abuse_button.vue';
-
-export const initReportAbuse = () => {
- const el = document.getElementById('js-report-abuse');
-
- if (!el) return false;
-
- const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset;
-
- return new Vue({
- el,
- name: 'ReportAbuseButtonRoot',
- provide: {
- reportAbusePath,
- reportedUserId: reportedUserId ? parseInt(reportedUserId, 10) : null,
- reportedFromUrl,
- },
- render(createElement) {
- return createElement(ReportAbuseButton);
- },
- });
-};
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 974b53caa15..524f2c045e6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,6 +1,7 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { createAlert } from '~/alert';
+import { visitUrl } from '~/lib/utils/url_utility';
import { STATUS_MERGED } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
@@ -114,6 +115,13 @@ export default {
return this.userHasApproved && !this.userCanApprove && this.mr.state !== STATUS_MERGED;
},
approvalText() {
+ // Repeating a text of this to keep i18n easier to do (vs, construcing a compound string)
+ if (this.requireSamlAuthToApprove) {
+ return this.isApproved && this.approvedBy.length > 0
+ ? s__('mrWidget|Approve additionally with SAML')
+ : s__('mrWidget|Approve with SAML');
+ }
+
return this.isApproved && this.approvedBy.length > 0
? s__('mrWidget|Approve additionally')
: s__('mrWidget|Approve');
@@ -161,14 +169,20 @@ export default {
.join(', ')
.concat('.');
},
+ requireSamlAuthToApprove() {
+ return this.mr.requireSamlAuthToApprove;
+ },
},
methods: {
approve() {
+ if (this.requireSamlAuthToApprove) {
+ this.approveWithSamlAuth();
+ return;
+ }
if (this.requirePasswordToApprove) {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
return;
}
-
this.updateApproval(
() => this.service.approveMergeRequest(),
() =>
@@ -179,6 +193,10 @@ export default {
),
);
},
+ approveWithSamlAuth() {
+ // Intentionally direct to SAML Identity Provider for renewed authorization even if SSO session exists
+ visitUrl(this.mr.samlApprovalPath);
+ },
approveWithAuth(data) {
this.updateApproval(
() => this.service.approveMergeRequestWithAuth(data),
@@ -236,7 +254,7 @@ export default {
};
</script>
<template>
- <div class="js-mr-approvals mr-section-container mr-widget-workflow">
+ <div v-if="approvals" class="js-mr-approvals mr-section-container mr-widget-workflow">
<state-container
:is-loading="$apollo.queries.approvals.loading"
:mr="mr"
@@ -258,7 +276,7 @@ export default {
:category="action.category"
:loading="isApproving"
class="gl-mr-3"
- data-qa-selector="approve_button"
+ data-testid="approve-button"
@click="action.action"
>
{{ action.text }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
index 367395f4446..b2c44dee230 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals_summary.vue
@@ -137,7 +137,7 @@ export default {
</script>
<template>
- <div data-qa-selector="approvals_summary_content">
+ <div data-testid="approvals-summary-content">
<span class="gl-font-weight-bold">{{ approvalLeftMessage }}</span>
<template v-if="hasApprovers">
<span v-if="approvalLeftMessage">{{ message }}</span>
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
index 303952c787e..32c3f19014b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/conflicts.vue
@@ -72,6 +72,8 @@ export default {
<template>
<merge-checks-message :check="check">
- <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ <template #failed>
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
</merge-checks-message>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
new file mode 100644
index 00000000000..431348e1d57
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -0,0 +1,6 @@
+export const COMPONENTS = {
+ conflict: () => import('./conflicts.vue'),
+ unresolved_discussions: () => import('./unresolved_discussions.vue'),
+ need_rebase: () => import('./rebase.vue'),
+ default: () => import('./message.vue'),
+};
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
index d0d749aa441..058b9e1fe99 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -1,10 +1,25 @@
<script>
+import { __ } from '~/locale';
import StatusIcon from '../widget/status_icon.vue';
const ICON_NAMES = {
failed: 'failed',
- allowed_to_fail: 'neutral',
- passed: 'success',
+ inactive: 'neutral',
+ success: 'success',
+};
+
+const FAILURE_REASONS = {
+ broken_status: __('Cannot merge the source into the target branch, due to a conflict.'),
+ ci_must_pass: __('Pipeline must succeed.'),
+ conflict: __('Merge conflicts must be resolved.'),
+ discussions_not_resolved: __('Unresolved discussions must be resolved.'),
+ draft_status: __('Merge request must not be draft.'),
+ not_open: __('Merge request must be open.'),
+ need_rebase: __('Merge request must be rebased, because a fast-forward merge is not possible.'),
+ not_approved: __('All required approvals must be given.'),
+ policies_denied: __('Denied licenses must be removed or approved.'),
+ merge_request_blocked: __('Merge request is blocked by another merge request.'),
+ status_checks_must_pass: __('Status checks must pass.'),
};
export default {
@@ -25,7 +40,10 @@ export default {
},
computed: {
iconName() {
- return ICON_NAMES[this.check.result];
+ return ICON_NAMES[this.check.status.toLowerCase()];
+ },
+ failureReason() {
+ return FAILURE_REASONS[this.check.identifier.toLowerCase()];
},
},
};
@@ -36,9 +54,10 @@ export default {
<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 class="gl-display-flex">{{ failureReason }}</div>
</div>
<slot></slot>
+ <slot v-if="check.status === 'FAILED'" name="failed"></slot>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
new file mode 100644
index 00000000000..c0ac1818ffa
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
@@ -0,0 +1,85 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import rebaseStateQuery from '../../queries/states/rebase.query.graphql';
+import Rebase from './rebase.vue';
+
+const service = {
+ rebase: () => new Promise(() => {}),
+};
+
+const defaultRender = ({ apolloProvider, check, mr, canCreatePipelineInTargetProject }) => ({
+ components: { Rebase },
+ apolloProvider,
+ provide: {
+ canCreatePipelineInTargetProject,
+ },
+ data() {
+ return { service, mr: { ...mr, targetProjectFullPath: 'gitlab-org/gitlab' }, check };
+ },
+ template: '<rebase :mr="mr" :service="service" :check="check" />',
+});
+
+const Template = ({
+ failed,
+ pushToSourceBranch,
+ rebaseInProgress,
+ onlyAllowMergeIfPipelineSucceeds,
+ canCreatePipelineInTargetProject,
+}) => {
+ const requestHandlers = [
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ identifier: 'need_rebase',
+ status: failed ? 'failed' : 'passed',
+ },
+ mr: { onlyAllowMergeIfPipelineSucceeds },
+ canCreatePipelineInTargetProject,
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ failed: true,
+ pushToSourceBranch: true,
+ rebaseInProgress: false,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ canCreatePipelineInTargetProject: false,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/rebase',
+ component: Rebase,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
new file mode 100644
index 00000000000..72140c22a89
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
@@ -0,0 +1,220 @@
+<script>
+import { GlModal, GlLink } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import simplePoll from '~/lib/utils/simple_poll';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import rebaseQuery from '../../queries/states/rebase.query.graphql';
+import eventHub from '../../event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksRebase',
+ components: {
+ GlModal,
+ GlLink,
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: rebaseQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data.project.mergeRequest,
+ },
+ },
+ inject: {
+ canCreatePipelineInTargetProject: {
+ default: false,
+ },
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ service: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ state: {},
+ isMakingRequest: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ rebaseInProgress() {
+ return this.state.rebaseInProgress;
+ },
+ showRebaseWithoutPipeline() {
+ return (
+ !this.mr.onlyAllowMergeIfPipelineSucceeds ||
+ (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline)
+ );
+ },
+ isForkMergeRequest() {
+ return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
+ },
+ isLatestPipelineCreatedInTargetProject() {
+ const latestPipeline = this.state.pipelines.nodes[0];
+
+ return latestPipeline?.project?.fullPath === this.mr.targetProjectFullPath;
+ },
+ shouldShowSecurityWarning() {
+ return (
+ this.canCreatePipelineInTargetProject &&
+ this.isForkMergeRequest &&
+ !this.isLatestPipelineCreatedInTargetProject
+ );
+ },
+ tertiaryActionsButtons() {
+ if (this.check.result === 'success') return [];
+
+ return [
+ {
+ text: s__('mrWidget|Rebase'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'standard-rebase-button',
+ onClick: () => this.tryRebase(),
+ },
+ this.showRebaseWithoutPipeline && {
+ text: s__('mrWidget|Rebase without pipeline'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'rebase-without-ci-button',
+ onClick: () => this.rebaseWithoutCi(),
+ },
+ ].filter((b) => b);
+ },
+ },
+ methods: {
+ rebase({ skipCi = false } = {}) {
+ this.isMakingRequest = true;
+
+ this.service
+ .rebase({ skipCi })
+ .then(() => simplePoll(this.checkRebaseStatus))
+ .catch((error) => {
+ this.isMakingRequest = false;
+
+ if (!error.response?.data?.merge_error) {
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ }
+ });
+ },
+ rebaseWithoutCi() {
+ return this.rebase({ skipCi: true });
+ },
+ tryRebase() {
+ if (this.shouldShowSecurityWarning) {
+ this.$refs.modal.show();
+ } else {
+ this.rebase();
+ }
+ },
+ checkRebaseStatus(continuePolling, stopPolling) {
+ this.service
+ .poll()
+ .then((res) => res.data)
+ .then((res) => {
+ if (res.rebase_in_progress || res.should_be_rebased) {
+ continuePolling();
+ } else {
+ this.isMakingRequest = false;
+
+ if (!res.merge_error?.length) {
+ toast(__('Rebase completed'));
+ }
+
+ eventHub.$emit('MRWidgetRebaseSuccess');
+ stopPolling();
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ stopPolling();
+ });
+ },
+ },
+ modal: {
+ id: 'rebase-security-risk-modal',
+ title: s__('mrWidget|Are you sure you want to rebase?'),
+ actionPrimary: {
+ text: s__('mrWidget|Rebase'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
+ runPipelinesInTheParentProjectHelpPath: helpPagePath(
+ '/ci/pipelines/merge_request_pipelines.html',
+ {
+ anchor: 'run-pipelines-in-the-parent-project',
+ },
+ ),
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="rebase"
+ >
+ <p>
+ {{
+ s__(
+ 'Pipelines|Rebasing creates a pipeline that runs code originating from a forked project merge request. Consequently there are potential security implications, such as the exposure of CI variables.',
+ )
+ }}
+ </p>
+ <p>
+ {{
+ s__(
+ "Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources.",
+ )
+ }}
+ </p>
+ <p>
+ {{ s__('Pipelines|If you are unsure, ask a project maintainer to review it for you.') }}
+ </p>
+ <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
+ {{ s__('Pipelines|More Information') }}
+ </gl-link>
+ </gl-modal>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue
new file mode 100644
index 00000000000..a6970d9c795
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/unresolved_discussions.vue
@@ -0,0 +1,39 @@
+<script>
+import { s__ } from '~/locale';
+import notesEventHub from '~/notes/event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksUnresolvedDiscussions',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ tertiaryActionsButtons() {
+ return [
+ {
+ text: s__('mrWidget|Go to first unresolved thread'),
+ category: 'default',
+ onClick: () => notesEventHub.$emit('jumpToFirstUnresolvedDiscussion'),
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 3e2f3ab4103..0f692f23142 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIntersectionObserver } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { sprintf, s__, __ } from '~/locale';
@@ -102,7 +102,7 @@ export default {
return this.statusIcon(this.collapsedData);
},
tertiaryActionsButtons() {
- return this.tertiaryButtons ? this.tertiaryButtons() : undefined;
+ return 'tertiaryButtons' in this ? this.tertiaryButtons() : undefined;
},
hydratedSummary() {
const structuredOutput = this.summary(this.collapsedData);
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
index 1c57226f887..77dc5b1d0da 100644
--- 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
@@ -15,9 +15,9 @@ const defaultRender = (apolloProvider) => ({
components: { MergeChecks },
apolloProvider,
data() {
- return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
+ return { service: {}, mr: { conflictResolutionPath: 'https://gitlab.com' } };
},
- template: '<merge-checks :mr="mr" />',
+ template: '<merge-checks :mr="mr" :service="service" />',
});
const Template = ({ canMerge, failed, pushToSourceBranch }) => {
@@ -32,16 +32,14 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => {
mergeRequest: {
id: 1,
userPermissions: { canMerge },
- mergeChecks: [
+ mergeabilityChecks: [
{
- failureReason: 'Unresolved discussions',
- identifier: 'unresolved_discussions',
- result: failed ? 'failed' : 'passed',
+ identifier: 'DISCUSSIONS_NOT_RESOLVED',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
{
- failureReason: 'Resolve conflicts',
- identifier: 'conflicts',
- result: failed ? 'failed' : 'passed',
+ identifier: 'CONFLICT',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
],
},
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
index fa84c0a4a6f..ac403c2c6f2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -1,16 +1,12 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
-import { n__, __, sprintf } from '~/locale';
+import { __, n__, sprintf } from '~/locale';
+import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
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: {
@@ -35,6 +31,10 @@ export default {
type: Object,
required: true,
},
+ service: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -68,10 +68,10 @@ export default {
);
},
checks() {
- return this.state.mergeChecks || [];
+ return this.state.mergeabilityChecks || [];
},
failedChecks() {
- return this.checks.filter((c) => c.result === 'failed');
+ return this.checks.filter((c) => c.status.toLowerCase() === 'failed');
},
},
methods: {
@@ -79,7 +79,7 @@ export default {
this.collapsed = !this.collapsed;
},
checkComponent(check) {
- return COMPONENTS[check.identifier] || COMPONENTS.default;
+ return COMPONENTS[check.identifier.toLowerCase()] || COMPONENTS.default;
},
},
};
@@ -122,6 +122,7 @@ export default {
}"
:check="check"
:mr="mr"
+ :service="service"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 2e104f2b93b..efc74241941 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 CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.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: {
- CiBadgeLink,
+ CiIcon,
GlLink,
GlLoadingIcon,
GlIcon,
@@ -194,13 +194,7 @@ export default {
</p>
</template>
<template v-else-if="hasPipeline">
- <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"
- />
+ <ci-icon :status="status" 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">
@@ -208,7 +202,9 @@ export default {
data-testid="pipeline-info-container"
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">
+ <p
+ class="mr-pipeline-title gl-align-self-start 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"
>#{{ pipeline.id }}</gl-link
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index ea3f324b8f2..370e07b397c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -28,7 +28,7 @@ export default {
};
</script>
<template>
- <div class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-center gl-mr-3">
+ <div class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3">
<div class="gl-display-flex gl-m-auto">
<gl-icon v-if="isMerged" name="merge" :size="16" class="gl-text-blue-500" />
<gl-icon v-else-if="isClosed" name="merge-request-close" :size="16" class="gl-text-red-500" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index 45958d7fb8d..c70213ad8a2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -99,11 +99,7 @@ export default {
<p class="gl-mt-2">
<gl-sprintf :message="$options.SP_HELP_CONTENT">
<template #link="{ content }">
- <gl-link
- data-testid="help"
- :href="$options.SP_HELP_URL"
- target="_blank"
- class="font-size-inherit"
+ <gl-link data-testid="help" :href="$options.SP_HELP_URL" target="_blank"
>{{ content }}
</gl-link>
</template>
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 ac434c5be4e..3c2d8efaffc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -1,10 +1,9 @@
<script>
import {
- GlIcon,
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormCheckbox,
GlSprintf,
GlLink,
@@ -15,6 +14,7 @@ import { isEmpty, isNil } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import { createAlert } from '~/alert';
+import { fetchPolicies } from '~/lib/graphql';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
@@ -25,11 +25,14 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
AUTO_MERGE_STRATEGIES,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
STATE_MACHINE,
+ MT_SKIP_TRAIN,
+ MT_RESTART_TRAIN,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -61,6 +64,10 @@ export default {
},
manual: true,
result({ data }) {
+ if (!data.project) {
+ return;
+ }
+
if (Object.keys(this.state).length === 0) {
this.removeSourceBranch =
data.project.mergeRequest.shouldRemoveSourceBranch ||
@@ -121,13 +128,12 @@ export default {
SquashBeforeMerge,
CommitEdit,
CommitMessageDropdown,
- GlIcon,
GlSprintf,
GlLink,
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormCheckbox,
GlSkeletonLoader,
MergeFailedPipelineConfirmationDialog,
@@ -139,6 +145,10 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
),
+ MergeTrainRestartTrainConfirmationDialog: () =>
+ import(
+ 'ee_component/vue_merge_request_widget/components/merge_train_restart_train_confirmation_dialog.vue'
+ ),
AddedCommitMessage,
RelatedLinks,
HelpPopover,
@@ -148,7 +158,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin],
+ mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin, glFeatureFlagsMixin()],
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
@@ -168,6 +178,10 @@ export default {
squashCommitMessageIsTouched: false,
isPipelineFailedModalVisibleMergeTrain: false,
isPipelineFailedModalVisibleNormalMerge: false,
+ isMergeTrainBeingForceMerged: false,
+ mergeTrainMergeType: MT_RESTART_TRAIN,
+ skipMergeTrain: false,
+ mergeTrainsSkipAllowed: this.mr.mergeTrainsSkipAllowed,
editCommitMessage: false,
};
},
@@ -319,6 +333,12 @@ export default {
title: this.autoMergePopoverSettings.title,
};
},
+ isSkipMergeTrainAvailable() {
+ return this.mergeTrainsSkipAllowed && this.glFeatures.mergeTrainsSkipTrain;
+ },
+ displaySkipMergeTrainOptions() {
+ return this.shouldDisplayMergeImmediatelyDropdownOptions && this.isSkipMergeTrainAvailable;
+ },
},
watch: {
'mr.state': function mrStateWatcher() {
@@ -329,6 +349,12 @@ export default {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
+
+ if (this.glFeatures.widgetPipelinePassSubscriptionUpdate) {
+ this.$apollo.queries.state.setOptions({
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+ }
},
beforeDestroy() {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
@@ -377,6 +403,7 @@ export default {
auto_merge_strategy: useAutoMerge ? this.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
+ skip_merge_train: this.skipMergeTrain,
};
// If users can't alter the squash message (e.g. for 1-commit merge requests),
@@ -428,6 +455,17 @@ export default {
this.handleMergeButtonClick(false, true);
}
},
+ handleMergeTrainMergeImmediatelyButtonClick(type) {
+ this.mergeTrainMergeType = type;
+ this.isMergeTrainBeingForceMerged = true;
+ },
+ processMergeTrain() {
+ if (this.mergeTrainMergeType === MT_SKIP_TRAIN) {
+ this.skipMergeTrain = true;
+ }
+
+ this.handleMergeButtonClick(false, true, true);
+ },
onMergeImmediatelyConfirmation() {
this.handleMergeButtonClick(false, true, true);
},
@@ -491,6 +529,8 @@ export default {
sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch.'),
divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
+ MT_SKIP_TRAIN,
+ MT_RESTART_TRAIN,
};
</script>
@@ -520,7 +560,7 @@ export default {
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
<div
- class="gl-display-flex gl-sm-flex-direction-column gl-md-align-items-center gl-flex-wrap gl-w-full"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-md-align-items-center gl-flex-wrap gl-w-full"
>
<gl-form-checkbox
v-if="canRemoveSourceBranch"
@@ -637,32 +677,57 @@ export default {
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="shouldShowMergeImmediatelyDropdown"
v-gl-tooltip.hover.focus="__('Select merge moment')"
:disabled="isMergeButtonDisabled"
variant="confirm"
+ class="gl-mr-0"
data-testid="merge-immediately-dropdown"
+ icon="chevron-down"
toggle-class="btn-icon js-merge-moment"
+ :toggle-text="__('Select a merge moment')"
+ text-sr-only
+ no-caret
>
- <template #button-content>
- <gl-icon name="chevron-down" class="mr-0" />
- <span class="sr-only">{{ __('Select merge moment') }}</span>
- </template>
- <gl-dropdown-item
- icon-name="warning"
- button-class="accept-merge-request"
+ <gl-disclosure-dropdown-item
+ v-if="
+ !shouldDisplayMergeImmediatelyDropdownOptions || !isSkipMergeTrainAvailable
+ "
data-testid="merge-immediately-button"
- @click="handleMergeImmediatelyButtonClick"
+ @action="handleMergeImmediatelyButtonClick"
+ >
+ <template #list-item> {{ __('Merge immediately') }} </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="displaySkipMergeTrainOptions"
+ data-testid="mt-merge-now-restart-button"
+ @action="handleMergeTrainMergeImmediatelyButtonClick($options.MT_RESTART_TRAIN)"
>
- {{ __('Merge immediately') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item>
+ <strong>{{ __(`Merge now and restart train`) }}</strong>
+ <p class="gl-text-gray-400 gl-font-sm gl-mb-0">
+ {{ __('Restart merge train pipelines with the merged changes.') }}
+ </p>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-disclosure-dropdown-item
+ v-if="displaySkipMergeTrainOptions"
+ data-testid="mt-merge-now-skip-restart-button"
+ @action="handleMergeTrainMergeImmediatelyButtonClick($options.MT_SKIP_TRAIN)"
+ >
+ <template #list-item>
+ <strong>{{ __(`Merge now and don't restart train`) }}</strong>
+ <p class="gl-text-gray-400 gl-font-sm gl-mb-0">
+ {{ __('Merge train pipelines continue without the merged changes.') }}
+ </p>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
</gl-button-group>
<template v-if="showAutoMergeHelperText">
<div
class="gl-ml-4 gl-text-gray-500 gl-font-sm"
- data-qa-selector="auto_merge_helper_text"
data-testid="auto-merge-helper-text"
>
{{ autoMergeHelperText }}
@@ -730,12 +795,16 @@ export default {
class="mr-ready-merge-related-links gl-display-inline"
/>
</li>
+ <li v-if="state.autoMergeEnabled" class="gl-line-height-normal">
+ {{ s__('mrWidget|Auto-merge enabled') }}
+ </li>
</ul>
</div>
</div>
</div>
</div>
<merge-immediately-confirmation-dialog
+ v-if="mr.mergeImmediatelyDocsPath"
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
@@ -745,6 +814,13 @@ export default {
@startMergeTrain="onStartMergeTrainConfirmation"
@cancel="isPipelineFailedModalVisibleMergeTrain = false"
/>
+ <merge-train-restart-train-confirmation-dialog
+ v-if="isSkipMergeTrainAvailable"
+ :visible="isMergeTrainBeingForceMerged"
+ :merge-train-type="mergeTrainMergeType"
+ @processMergeTrainMerge="processMergeTrain"
+ @cancel="isMergeTrainBeingForceMerged = false"
+ />
<merge-failed-pipeline-confirmation-dialog
:visible="isPipelineFailedModalVisibleNormalMerge"
@mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index 7fc4a06cbae..267facb0a50 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -143,7 +143,9 @@ export default {
:collapsed="mr.mergeDetailsCollapsed"
@toggle="() => mr.toggleMergeDetails()"
>
- <span class="gl-ml-0! gl-text-body! gl-flex-grow-1">
+ <span
+ class="gl-display-inline-flex gl-align-self-start gl-pt-2 gl-ml-0! gl-text-body! gl-flex-grow-1"
+ >
<bold-text :message="$options.i18n.removeDraftStatus" />
</span>
<template #actions>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 8249dffcc27..08e803bffc9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -9,6 +9,8 @@ export default {
MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'),
MrCodeQualityWidget: () =>
import('~/vue_merge_request_widget/extensions/code_quality/index.vue'),
+ MrAccessibilityWidget: () =>
+ import('~/vue_merge_request_widget/extensions/accessibility/index.vue'),
},
props: {
@@ -31,12 +33,17 @@ export default {
return this.mr.codequalityReportsPath ? 'MrCodeQualityWidget' : undefined;
},
+ accessibilityWidget() {
+ return this.mr.accessibilityReportPath ? 'MrAccessibilityWidget' : undefined;
+ },
+
widgets() {
return [
this.codeQualityWidget,
this.testReportWidget,
this.terraformPlansWidget,
'MrSecurityWidget',
+ this.accessibilityWidget,
].filter((w) => w);
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index 72c041759d9..d4375690ad1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlLink } from '@gitlab/ui';
+import { GlBadge, GlLink, GlTooltipDirective } from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateText } from '../extensions/utils';
import ContentRow from './widget_content_row.vue';
@@ -15,6 +15,7 @@ export default {
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
props: {
data: {
@@ -78,7 +79,11 @@ export default {
<div class="gl-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline">
<div>
- <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p>
+ <p
+ v-gl-tooltip="{ title: data.tooltipText, boundary: 'viewport' }"
+ v-safe-html="generatedText"
+ class="gl-mb-0 gl-mr-1"
+ ></p>
<gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
<p
v-if="data.supportingText"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index d17be3e4037..0eb50b9ff4f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { logError } from '~/lib/logger';
import SafeHtml from '~/vue_shared/directives/safe_html';
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 1a469f9b7bb..071f95a28fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -6,7 +6,7 @@ import { stateToComponentMap as classStateMap, stateKey } from './stores/state_m
export const FOUR_MINUTES_IN_MS = 1000 * 60 * 4;
export const STATE_QUERY_POLLING_INTERVAL_DEFAULT = 5000;
-export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 2;
+export const STATE_QUERY_POLLING_INTERVAL_BACKOFF = 1.2;
export const SUCCESS = 'success';
export const WARNING = 'warning';
@@ -202,3 +202,6 @@ export const DETAILED_MERGE_STATUS = {
CI_STILL_RUNNING: 'CI_STILL_RUNNING',
EXTERNAL_STATUS_CHECKS: 'EXTERNAL_STATUS_CHECKS',
};
+
+export const MT_SKIP_TRAIN = 'skip';
+export const MT_RESTART_TRAIN = 'restart';
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue
index 0fb5e13ad82..2ae16eef410 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/accessibility/index.vue
@@ -1,24 +1,37 @@
+<script>
import { uniqueId } from 'lodash';
import { __, n__, s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import { EXTENSION_ICONS } from '../../constants';
export default {
name: 'WidgetAccessibility',
- enablePolling: true,
i18n: {
loading: s__('Reports|Accessibility scanning results are being parsed'),
error: s__('Reports|Accessibility scanning failed loading results'),
},
- props: ['accessibilityReportPath'],
+ components: {
+ MrWidget,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsedData: {},
+ content: [],
+ };
+ },
computed: {
statusIcon() {
- return this.collapsedData.status === 'failed'
+ return this.collapsedData?.status === 'failed'
? EXTENSION_ICONS.warning
: EXTENSION_ICONS.success;
},
- },
- methods: {
summary() {
const numOfResults = this.collapsedData?.summary?.errored || 0;
@@ -37,13 +50,20 @@ export default {
false,
);
- return numOfResults === 0 ? successText : warningText;
+ return numOfResults === 0 ? { title: successText } : { title: warningText };
},
shouldCollapse() {
return this.collapsedData?.summary?.errored > 0;
},
+ },
+ methods: {
fetchCollapsedData() {
- return axios.get(this.accessibilityReportPath);
+ return axios.get(this.mr.accessibilityReportPath).then((response) => {
+ this.collapsedData = response.data;
+ this.content = this.getContent(response.data);
+
+ return response;
+ });
},
fetchFullData() {
return Promise.resolve(this.prepareReports());
@@ -74,9 +94,7 @@ export default {
formatMessage(message) {
return sprintf(s__('AccessibilityReport|Message: %{message}'), { message });
},
- prepareReports() {
- const { collapsedData } = this;
-
+ getContent(collapsedData) {
const newErrors = collapsedData.new_errors.map((error) => {
return {
header: __('New'),
@@ -121,3 +139,16 @@ export default {
},
},
};
+</script>
+<template>
+ <mr-widget
+ :error-text="$options.i18n.error"
+ :status-icon-name="statusIcon"
+ :loading-text="$options.i18n.loading"
+ :widget-name="$options.name"
+ :summary="summary"
+ :content="content"
+ :is-collapsible="shouldCollapse"
+ :fetch-collapsed-data="fetchCollapsedData"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
index cd3a98effa3..e87b5d20ca0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
index e7d8de97f20..a36a58c68de 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, sprintf } from '~/locale';
@@ -10,11 +10,7 @@ export default {
name: 'WidgetSecurityReportsCE',
components: {
MrWidget,
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlTooltip,
+ GlDisclosureDropdown,
},
i18n: {
apiError: s__(
@@ -76,17 +72,23 @@ export default {
summary() {
return { title: this.$options.i18n.scansHaveRun };
},
+ listboxOptions() {
+ return this.artifacts.map(({ name, path }) => ({
+ text: sprintf(s__('SecurityReports|Download %{artifactName}'), {
+ artifactName: name,
+ }),
+ href: path,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ },
+ }));
+ },
},
methods: {
handleIsLoading(value) {
this.isLoading = value;
},
-
- artifactText({ name }) {
- return sprintf(s__('SecurityReports|Download %{artifactName}'), {
- artifactName: name,
- });
- },
},
widgetHelpPopover: {
options: { title: s__('ciReport|Security scan results') },
@@ -116,26 +118,12 @@ export default {
@is-loading="handleIsLoading"
>
<template #action-buttons>
- <div class="gl-ml-3">
- <gl-dropdown
- v-gl-tooltip
- icon="download"
- size="small"
- category="tertiary"
- variant="confirm"
- right
- >
- <gl-dropdown-item
- v-for="artifact in artifacts"
- :key="artifact.path"
- :href="artifact.path"
- :data-testid="`download-${artifact.name}`"
- download
- >
- {{ artifactText(artifact) }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
+ <gl-disclosure-dropdown
+ class="gl-ml-3"
+ size="small"
+ icon="download"
+ :items="listboxOptions"
+ />
</template>
</mr-widget>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
index 1b03b9c04e1..c12bc6456a5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
@@ -19,7 +19,9 @@ import {
import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
export default {
- name: 'WidgetTestReport',
+ // widget name does not match file path because widget name must match telemetry event names
+ // see https://gitlab.com/gitlab-org/gitlab/-/issues/427061
+ name: 'WidgetTestSummary',
components: {
MrWidget,
MrWidgetRow,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
index 564e9321d54..8bb2f2898eb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/approvals.js
@@ -18,8 +18,16 @@ export default {
iid: `${this.mr.iid}`,
};
},
- update: (data) => data.project.mergeRequest,
+ update: (data) => data.project?.mergeRequest,
result({ data }) {
+ // This case can occur when backend returns an empty project due to expired session.
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/413627 for more information.
+ if (!data.project) {
+ // Needed to suppress several errors.
+ this.mr.setApprovals({});
+ return;
+ }
+
const { mergeRequest } = data.project;
this.disableCommittersApproval = data.project.mergeRequestsDisableCommittersApproval;
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 2f49252a06b..623b504fcc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -21,13 +21,6 @@ export default {
this.mr.preventMerge,
);
},
- mergeDisabledText() {
- if (this.pipeline?.status === PIPELINE_SKIPPED_STATUS) {
- return MERGE_DISABLED_SKIPPED_PIPELINE_TEXT;
- }
-
- return MERGE_DISABLED_TEXT;
- },
pipelineMustSucceedConflictText() {
return PIPELINE_MUST_SUCCEED_CONFLICT_TEXT;
},
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 02d73cf9cbd..cc116b42f1e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,9 +1,6 @@
<script>
import { isEmpty, clamp } from 'lodash';
-import {
- registerExtension,
- registeredExtensions,
-} from '~/vue_merge_request_widget/components/extensions';
+import { registeredExtensions } from '~/vue_merge_request_widget/components/extensions';
import SafeHtml from '~/vue_shared/directives/safe_html';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
@@ -55,7 +52,6 @@ import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
-import accessibilityExtension from './extensions/accessibility';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@@ -235,9 +231,6 @@ export default {
false,
);
},
- shouldShowAccessibilityReport() {
- return Boolean(this.mr?.accessibilityReportPath);
- },
formattedHumanAccess() {
return (this.mr.humanAccess || '').toLowerCase();
},
@@ -268,11 +261,6 @@ export default {
this.initPostMergeDeploymentsPolling();
}
},
- shouldShowAccessibilityReport(newVal) {
- if (newVal) {
- this.registerAccessibilityExtension();
- }
- },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -507,11 +495,6 @@ export default {
dismissSuggestPipelines() {
this.mr.isDismissedSuggestPipeline = true;
},
- registerAccessibilityExtension() {
- if (this.shouldShowAccessibilityReport) {
- registerExtension(accessibilityExtension);
- }
- },
},
};
</script>
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
index 6b602a0095c..fcaddcc2a42 100644
--- 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
@@ -6,7 +6,10 @@ query mergeChecks($projectPath: ID!, $iid: String!) {
userPermissions {
canMerge
}
- mergeChecks @client
+ mergeabilityChecks {
+ identifier
+ status
+ }
}
}
}
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 6803d609dbc..e84b3f53b53 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -10,7 +10,7 @@ import {
GlTab,
GlButton,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { fetchPolicies } from '~/lib/graphql';
@@ -30,7 +30,7 @@ import AlertSidebar from './alert_sidebar.vue';
import AlertSummaryRow from './alert_summary_row.vue';
import SystemNote from './system_notes/system_note.vue';
-const containerEl = document.querySelector('.page-with-contextual-sidebar');
+const containerEl = document.querySelector('.layout-page');
export default {
i18n: {
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 2d3815439a6..056388f690d 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -284,13 +284,7 @@ export default {
>
<div v-if="userName" class="gl-display-inline-flex gl-mt-2" data-testid="assigned-users">
<span class="gl-relative gl-mr-4">
- <img
- :alt="userName"
- :src="userImg"
- :width="32"
- class="avatar avatar-inline gl-m-0 s32"
- data-qa-selector="avatar_image"
- />
+ <img :alt="userName" :src="userImg" :width="32" class="avatar avatar-inline gl-m-0 s32" />
</span>
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
<strong class="dropdown-menu-user-full-name">
diff --git a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
index ffbcdefc924..93e1fc4a0c2 100644
--- a/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
+++ b/app/assets/javascripts/vue_shared/components/chronic_duration_input.vue
@@ -1,6 +1,6 @@
<script>
-import * as Sentry from '@sentry/browser';
import { GlFormInput } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import {
DurationParseError,
outputChronicDuration,
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
deleted file mode 100644
index abbeac0e098..00000000000
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ /dev/null
@@ -1,157 +0,0 @@
-<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from './ci_icon.vue';
-
-/**
- * Renders CI Badge link with CI icon and status text based on
- * API response shared between all places where it is used.
- *
- * Receives status object containing:
- * status: {
- * details_path or detailsPath: "/gitlab-org/gitlab-foss/pipelines/8150156" // url
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
- * label:"running" // used for potential tooltip
- * text:"running" // text rendered
- * }
- *
- * Used in:
- * - Pipelines table - first column
- * - Jobs table - first column
- * - Pipeline show view - header
- * - Job show view - header
- * - MR widget
- * - Terraform table
- * - On-demand scans list
- */
-
-const badgeSizeOptions = {
- sm: 'sm',
- md: 'md',
- lg: 'lg',
-};
-
-export default {
- components: {
- CiIcon,
- GlBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- status: {
- type: Object,
- required: true,
- },
- showText: {
- type: Boolean,
- required: false,
- default: true,
- },
- size: {
- type: String,
- required: false,
- default: badgeSizeOptions.md,
- validator(value) {
- return badgeSizeOptions[value] !== undefined;
- },
- },
- showTooltip: {
- type: Boolean,
- required: false,
- default: true,
- },
- useLink: {
- type: Boolean,
- default: true,
- required: false,
- },
- },
- computed: {
- isNotLargeBadgeSize() {
- return this.size !== badgeSizeOptions.lg;
- },
- title() {
- 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() {
- switch (this.status.icon) {
- case 'status_success':
- return {
- textColor: 'gl-text-green-700',
- variant: 'success',
- };
- case 'status_warning':
- return {
- textColor: 'gl-text-orange-700',
- variant: 'warning',
- };
- case 'status_failed':
- return {
- textColor: 'gl-text-red-700',
- variant: 'danger',
- };
- case 'status_running':
- return {
- textColor: 'gl-text-blue-700',
- variant: 'info',
- };
- case 'status_pending':
- return {
- textColor: 'gl-text-orange-700',
- variant: 'warning',
- };
- case 'status_canceled':
- return {
- textColor: 'gl-text-gray-700',
- variant: 'neutral',
- };
- case 'status_manual':
- return {
- textColor: 'gl-text-gray-700',
- variant: 'neutral',
- };
- // default covers the styles for the remainder of CI
- // statuses that are not explicitly stated here
- default:
- return {
- textColor: 'gl-text-gray-600',
- variant: 'muted',
- };
- }
- },
- },
-};
-</script>
-<template>
- <gl-badge
- v-gl-tooltip
- :class="{ 'gl-px-2': !showText && isNotLargeBadgeSize }"
- :title="title"
- :href="detailsPath"
- :size="size"
- :variant="badgeStyles.variant"
- data-testid="ci-badge-link"
- @click="$emit('ciStatusBadgeClick')"
- >
- <ci-icon :status="status" />
-
- <template v-if="showText">
- <span
- class="gl-ml-2 gl-white-space-nowrap"
- :class="badgeStyles.textColor"
- data-testid="ci-badge-text"
- >
- {{ status.text }}
- </span>
- </template>
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 6670b931416..a2b6b4642c9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -1,99 +1,115 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
/**
* Renders CI icon based on API response shared between all places where it is used.
*
* Receives status object containing:
* status: {
- * group:"running" // used for CSS class
- * icon: "icon_status_running" // used to render the icon
+ * icon: "status_running" // used to render the icon and CSS class
+ * text: "Running",
+ * detailsPath: '/project1/jobs/1' // can also be details_path
* }
*
- * Used in:
- * - Extended MR Popover
- * - Jobs show view header
- * - Jobs show view sidebar
- * - Jobs table
- * - Linked pipelines
- * - Pipeline graph
- * - Pipeline mini graph
- * - Pipeline show view badge
- * - Pipelines table Badge
*/
-/*
- * These sizes are defined in gitlab-ui/src/scss/variables.scss
- * under '$gl-icon-sizes'
- */
-const validSizes = [8, 12, 14, 16, 24, 32, 48, 72];
-
export default {
components: {
+ GlBadge,
GlIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
status: {
type: Object,
required: true,
validator(status) {
- const { group, icon } = status;
- return (
- typeof group === 'string' &&
- group.length &&
- typeof icon === 'string' &&
- icon.startsWith('status_')
- );
+ const { icon } = status;
+ return typeof icon === 'string' && icon.startsWith('status_');
},
},
- size: {
- type: Number,
- required: false,
- default: 16,
- validator(value) {
- return validSizes.includes(value);
- },
- },
- isActive: {
+ showStatusText: {
type: Boolean,
required: false,
default: false,
},
- isBorderless: {
+ showTooltip: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
- isInteractive: {
+ useLink: {
type: Boolean,
+ default: true,
required: false,
- default: false,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
},
},
computed: {
- wrapperStyleClasses() {
- const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`;
+ title() {
+ if (this.showTooltip) {
+ // show tooltip only when not showing text already
+ return !this.showStatusText ? this.status?.text : null;
+ }
+ return null;
+ },
+ ariaLabel() {
+ // show aria-label only when text is not rendered
+ if (!this.showStatusText) {
+ return this.status?.text;
+ }
+ return null;
+ },
+ href() {
+ // href can come from GraphQL (camelCase) or REST API (snake_case)
+ if (this.useLink) {
+ return this.status.detailsPath || this.status.details_path;
+ }
+ return null;
},
icon() {
- return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon;
+ if (this.status.icon) {
+ return `${this.status.icon}_borderless`;
+ }
+ return null;
+ },
+ variant() {
+ switch (this.status.icon) {
+ case 'status_success':
+ return 'success';
+ case 'status_warning':
+ case 'status_pending':
+ return 'warning';
+ case 'status_failed':
+ return 'danger';
+ case 'status_running':
+ return 'info';
+ // default covers the styles for the remainder of CI
+ // statuses that are not explicitly stated here
+ default:
+ return 'neutral';
+ }
},
},
};
</script>
<template>
- <span
- :class="[
- wrapperStyleClasses,
- { interactive: isInteractive, active: isActive, borderless: isBorderless },
- ]"
- :style="{ height: `${size}px`, width: `${size}px` }"
+ <gl-badge
+ v-gl-tooltip
+ class="ci-icon gl-p-2"
+ :class="`ci-icon-variant-${variant}`"
+ :variant="variant"
+ :title="title"
+ :aria-label="ariaLabel"
+ :href="href"
+ size="md"
+ data-testid="ci-icon"
+ @click="$emit('ciStatusBadgeClick')"
>
- <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
- </span>
+ <span class="ci-icon-gl-icon-wrapper"><gl-icon :name="icon" /></span
+ ><span v-if="showStatusText" class="gl-mx-2 gl-white-space-nowrap" data-testid="ci-icon-text">{{
+ status.text
+ }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
index f62bfb551df..55767c5f4bc 100644
--- a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlDisclosureDropdown, GlIcon, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { __, n__, s__, sprintf } from '~/locale';
@@ -16,12 +10,16 @@ export const i18n = {
searchFiles: __('Search files'),
};
+const variantCssColorMap = {
+ success: 'gl-text-green-500',
+ danger: 'gl-text-red-500',
+};
+
export default {
i18n,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
+ GlDisclosureDropdown,
+ GlIcon,
GlSearchBoxByType,
GlSprintf,
},
@@ -54,6 +52,15 @@ export default {
? fuzzaldrinPlus.filter(this.files, this.search, { key: 'name' })
: this.files;
},
+ dropdownItems() {
+ return this.filteredFiles.map((file) => {
+ return {
+ ...file,
+ text: file.name || this.$options.i18n.noFileNameAvailable,
+ iconColor: variantCssColorMap[file.iconColor],
+ };
+ });
+ },
messageChanged() {
return sprintf(
n__(
@@ -64,21 +71,21 @@ export default {
{ count: this.changed },
);
},
-
- additionsText() {
- return n__('Diffs|%d addition', 'Diffs|%d additions', this.added);
- },
- deletionsText() {
- return n__('Diffs|%d deletion', 'Diffs|%d deletions', this.deleted);
- },
},
methods: {
- jumpToFile(fileHash) {
- window.location.hash = fileHash;
- },
focusInput() {
this.$refs.search.focusInput();
},
+ focusFirstItem() {
+ if (!this.filteredFiles.length) return;
+ this.$el.querySelector('.gl-new-dropdown-item:first-child').focus();
+ },
+ additionsText(numberOfChanges = this.added) {
+ return n__('Diffs|%d addition', 'Diffs|%d additions', numberOfChanges);
+ },
+ deletionsText(numberOfChanges = this.deleted) {
+ return n__('Diffs|%d deletion', 'Diffs|%d deletions', numberOfChanges);
+ },
},
};
</script>
@@ -87,15 +94,15 @@ export default {
<div>
<gl-sprintf :message="messageChanged">
<template #dropdown="{ content: dropdownText }">
- <gl-dropdown
+ <gl-disclosure-dropdown
+ :toggle-text="dropdownText"
+ :items="dropdownItems"
category="tertiary"
variant="confirm"
- :text="dropdownText"
data-testid="diff-stats-dropdown"
class="gl-vertical-align-baseline"
toggle-class="gl-px-0! gl-font-weight-bold!"
- menu-class="gl-w-auto!"
- no-flip
+ fluid-width
@shown="focusInput"
>
<template #header>
@@ -103,35 +110,38 @@ export default {
ref="search"
v-model.trim="search"
:placeholder="$options.i18n.searchFiles"
+ class="gl-mx-3 gl-my-4"
+ @keydown.down="focusFirstItem"
/>
+ <span v-if="!filteredFiles.length" class="gl-mx-3">
+ {{ $options.i18n.noFilesFound }}
+ </span>
</template>
- <gl-dropdown-item
- v-for="file in filteredFiles"
- :key="file.href"
- :icon-name="file.icon"
- :icon-color="file.iconColor"
- @click="jumpToFile(file.href)"
- >
- <div class="gl-display-flex">
- <span v-if="file.name" class="gl-font-weight-bold gl-mr-3 gl-text-truncate">{{
- file.name
- }}</span>
- <span v-else class="gl-mr-3 gl-font-weight-bold gl-font-style-italic gl-gray-400">{{
- $options.i18n.noFileNameAvailable
- }}</span>
- <span class="gl-ml-auto gl-white-space-nowrap">
- <span class="gl-text-green-600">+{{ file.added }}</span>
- <span class="gl-text-red-500">-{{ file.removed }}</span>
- </span>
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center gl-overflow-hidden">
+ <gl-icon :name="item.icon" :class="item.iconColor" class="gl-flex-shrink-0" />
+ <div class="gl-flex-grow-1 gl-overflow-hidden">
+ <div class="gl-display-flex">
+ <span
+ class="gl-font-weight-bold gl-mr-3 gl-flex-grow-1"
+ :class="item.name ? 'gl-text-truncate' : 'gl-font-style-italic gl-gray-400'"
+ >{{ item.text }}</span
+ >
+ <span class="gl-ml-auto gl-white-space-nowrap" aria-hidden="true">
+ <span class="gl-text-green-600">+{{ item.added }}</span>
+ <span class="gl-text-red-500">-{{ item.removed }}</span>
+ </span>
+ <span class="gl-sr-only"
+ >{{ additionsText(item.added) }}, {{ deletionsText(item.removed) }}</span
+ >
+ </div>
+ <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
+ {{ item.path }}
+ </div>
+ </div>
</div>
- <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
- {{ file.path }}
- </div>
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredFiles.length">
- {{ $options.i18n.noFilesFound }}
- </gl-dropdown-text>
- </gl-dropdown>
+ </template>
+ </gl-disclosure-dropdown>
</template>
</gl-sprintf>
<span
@@ -140,12 +150,20 @@ export default {
>
<gl-sprintf :message="$options.i18n.messageAdditionsDeletions">
<template #additions>
- <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText }}</span>
+ <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText() }}</span>
</template>
<template #deletions>
- <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText }}</span>
+ <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText() }}</span>
</template>
</gl-sprintf>
</span>
</div>
</template>
+
+<style scoped>
+/* TODO: Use max-height prop when gitlab-ui got updated.
+See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
+::v-deep .gl-new-dropdown-inner {
+ max-height: 310px;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/constants.js b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
index 0fb5a2d5534..5bad907c9f9 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/constants.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/constants.js
@@ -14,3 +14,13 @@ export const PROJECT_TOGGLE_TEXT = s__('ProjectSelect|Search for project');
export const PROJECT_HEADER_TEXT = s__('ProjectSelect|Select a project');
export const FETCH_PROJECTS_ERROR = __('Unable to fetch projects. Reload the page to try again.');
export const FETCH_PROJECT_ERROR = __('Unable to fetch project. Reload the page to try again.');
+
+// Organizations
+export const ORGANIZATION_TOGGLE_TEXT = s__('Organization|Search for an organization');
+export const ORGANIZATION_HEADER_TEXT = s__('Organization|Select an organization');
+export const FETCH_ORGANIZATIONS_ERROR = s__(
+ 'Organization|Unable to fetch organizations. Reload the page to try again.',
+);
+export const FETCH_ORGANIZATION_ERROR = s__(
+ 'Organization|Unable to fetch organizations. Reload the page to try again.',
+);
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index 970c24c6e87..1a215454ab6 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -22,6 +22,11 @@ export default {
type: String,
required: true,
},
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
inputName: {
type: String,
required: true,
@@ -31,7 +36,7 @@ export default {
required: true,
},
initialSelection: {
- type: String,
+ type: [String, Number],
required: false,
default: null,
},
@@ -57,6 +62,11 @@ export default {
required: false,
default: null,
},
+ toggleClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -152,6 +162,7 @@ export default {
this.searching = true;
const name = await this.fetchInitialSelectionText(this.initialSelection);
+
this.selectedValue = this.initialSelection;
this.selectedText = name;
this.pristine = false;
@@ -178,7 +189,7 @@ export default {
</script>
<template>
- <gl-form-group :label="label">
+ <gl-form-group :label="label" :description="description">
<slot name="error"></slot>
<template v-if="Boolean($scopedSlots.label)" #label>
<slot name="label"></slot>
@@ -196,6 +207,7 @@ export default {
:no-results-text="noResultsText"
:infinite-scroll="hasMoreItems"
:infinite-scroll-loading="infiniteScrollLoading"
+ :toggle-class="toggleClass"
searchable
@shown="onShown"
@search="search"
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index eb7b20fa4c1..8a338551fbe 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import Api, { DEFAULT_PER_PAGE } from '~/api';
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
new file mode 100644
index 00000000000..d068d86d95b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
@@ -0,0 +1,150 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql';
+import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants';
+import {
+ ORGANIZATION_TOGGLE_TEXT,
+ ORGANIZATION_HEADER_TEXT,
+ FETCH_ORGANIZATIONS_ERROR,
+ FETCH_ORGANIZATION_ERROR,
+} from './constants';
+import EntitySelect from './entity_select.vue';
+
+export default {
+ name: 'OrganizationSelect',
+ components: {
+ GlAlert,
+ EntitySelect,
+ },
+ props: {
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ initialSelection: {
+ type: [String, Number],
+ required: false,
+ default: null,
+ },
+ clearable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ toggleClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ errorMessage: '',
+ };
+ },
+ methods: {
+ async fetchOrganizations() {
+ try {
+ const {
+ data: {
+ currentUser: {
+ organizations: { nodes },
+ },
+ },
+ } = await this.$apollo.query({
+ query: getCurrentUserOrganizationsQuery,
+ // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ });
+
+ return {
+ items: nodes.map((organization) => ({
+ text: organization.name,
+ value: getIdFromGraphQLId(organization.id),
+ })),
+ // TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ totalPages: 1,
+ };
+ } catch (error) {
+ this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error });
+
+ return { items: [], totalPages: 0 };
+ }
+ },
+ async fetchOrganizationName(id) {
+ try {
+ const {
+ data: {
+ organization: { name },
+ },
+ } = await this.$apollo.query({
+ query: getOrganizationQuery,
+ variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) },
+ });
+
+ return name;
+ } catch (error) {
+ this.handleError({ message: FETCH_ORGANIZATION_ERROR, error });
+
+ return '';
+ }
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
+ },
+ i18n: {
+ toggleText: ORGANIZATION_TOGGLE_TEXT,
+ selectGroup: ORGANIZATION_HEADER_TEXT,
+ },
+};
+</script>
+
+<template>
+ <entity-select
+ :block="block"
+ :label="label"
+ :description="description"
+ :input-name="inputName"
+ :input-id="inputId"
+ :initial-selection="initialSelection"
+ :clearable="clearable"
+ :header-text="$options.i18n.selectGroup"
+ :default-toggle-text="$options.i18n.toggleText"
+ :fetch-items="fetchOrganizations"
+ :fetch-initial-selection-text="fetchOrganizationName"
+ :toggle-class="toggleClass"
+ v-on="$listeners"
+ >
+ <template #error>
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
+ </template>
+ </entity-select>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
index 13a825a68f6..8c371e3d4ce 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import SafeHtml from '~/vue_shared/directives/safe_html';
import {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 346384e3023..d39e4d2ee42 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -292,7 +292,9 @@ export default {
this.recentSearchesService.save(resultantSearches);
this.recentSearches = [];
},
- handleFilterSubmit() {
+ async handleFilterSubmit() {
+ this.blurSearchInput();
+ await this.$nextTick();
const filterTokens = uniqueTokens(this.filterValue);
this.filterValue = filterTokens;
@@ -309,7 +311,6 @@ export default {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
});
}
- this.blurSearchInput();
this.$emit('onFilter', this.removeQuotesEnclosure(filterTokens));
},
historyTokenOptionTitle(historyToken) {
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 23de8dd5596..3857dd9c55d 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
@@ -7,9 +7,10 @@ import {
GlDropdownText,
GlLoadingIcon,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, last } from 'lodash';
import { stripQuotes } from '~/lib/utils/text_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
@@ -22,6 +23,7 @@ export default {
GlDropdownText,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
config: {
type: Object,
@@ -70,6 +72,11 @@ export default {
required: false,
default: undefined,
},
+ multiSelectValues: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -94,7 +101,11 @@ export default {
return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
activeTokenValue() {
- return this.getActiveTokenValue(this.suggestions, this.value.data);
+ const data =
+ this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data)
+ ? last(this.value.data)
+ : this.value.data;
+ return this.getActiveTokenValue(this.suggestions, data);
},
availableDefaultSuggestions() {
if ([OPERATOR_NOT, OPERATOR_OR].includes(this.value.operator)) {
@@ -146,10 +157,14 @@ export default {
watch: {
active: {
immediate: true,
- handler(newValue) {
- if (!newValue && !this.suggestions.length) {
- const search = this.searchTerm ? this.searchTerm : this.value.data;
- this.$emit('fetch-suggestions', search);
+ handler(active) {
+ if (!active && !this.suggestions.length) {
+ // data could be a string or an array of strings
+ const selectedItems = [this.value.data].flat();
+ selectedItems.forEach((item) => {
+ const search = this.searchTerm ? this.searchTerm : item;
+ this.$emit('fetch-suggestions', search);
+ });
}
},
},
@@ -163,6 +178,9 @@ export default {
},
methods: {
handleInput: debounce(function debouncedSearch({ data, operator }) {
+ // in multiSelect mode, data could be an array
+ if (Array.isArray(data)) return;
+
// Prevent fetching suggestions when data or operator is not present
if (data || operator) {
this.searchKey = data;
@@ -181,8 +199,11 @@ export default {
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
- const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
+ if (this.glFeatures.groupMultiSelectTokens) {
+ this.$emit('token-selected', selectedValue);
+ }
+ const activeTokenValue = this.getActiveTokenValue(this.suggestions, selectedValue);
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
@@ -210,6 +231,7 @@ export default {
:config="config"
:value="value"
:active="active"
+ :multi-select-values="multiSelectValues"
v-bind="$attrs"
v-on="$listeners"
@input="handleInput"
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 4601287b417..c5326ead60d 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
@@ -1,11 +1,12 @@
<script>
-import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { GlAvatar, GlIcon, GlIntersperse, GlFilteredSearchSuggestion } from '@gitlab/ui';
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 glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -14,8 +15,11 @@ export default {
components: {
BaseToken,
GlAvatar,
+ GlIcon,
+ GlIntersperse,
GlFilteredSearchSuggestion,
},
+ mixins: [glFeatureFlagMixin()],
props: {
config: {
type: Object,
@@ -32,8 +36,11 @@ export default {
},
data() {
return {
+ // current users visible in list
users: this.config.initialUsers || [],
+ allUsers: this.config.initialUsers || [],
loading: false,
+ selectedUsernames: [],
};
},
computed: {
@@ -49,13 +56,69 @@ export default {
fetchUsersQuery() {
return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
},
+ multiSelectEnabled() {
+ return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens;
+ },
+ },
+ watch: {
+ value: {
+ deep: true,
+ immediate: true,
+ handler(newValue) {
+ const { data } = newValue;
+
+ if (!this.multiSelectEnabled) {
+ return;
+ }
+
+ // don't add empty values to selectedUsernames
+ if (!data) {
+ return;
+ }
+
+ if (Array.isArray(data)) {
+ this.selectedUsernames = data;
+ // !active so we don't add strings while searching, e.g. r, ro, roo
+ // !includes so we don't add the same usernames (if @input is emitted twice)
+ } else if (!this.active && !this.selectedUsernames.includes(data)) {
+ this.selectedUsernames = this.selectedUsernames.concat(data);
+ }
+ },
+ },
},
methods: {
getActiveUser(users, data) {
return users.find((user) => user.username.toLowerCase() === data.toLowerCase());
},
getAvatarUrl(user) {
- return user.avatarUrl || user.avatar_url;
+ return user?.avatarUrl || user?.avatar_url;
+ },
+ displayNameFor(username) {
+ return this.getActiveUser(this.allUsers, username)?.name || `@${username}`;
+ },
+ avatarFor(username) {
+ const user = this.getActiveUser(this.allUsers, username);
+ return this.getAvatarUrl(user);
+ },
+ addCheckIcon(username) {
+ return this.multiSelectEnabled && this.selectedUsernames.includes(username);
+ },
+ addPadding(username) {
+ return this.multiSelectEnabled && !this.selectedUsernames.includes(username);
+ },
+ handleSelected(username) {
+ if (!this.multiSelectEnabled) {
+ return;
+ }
+
+ const index = this.selectedUsernames.indexOf(username);
+ if (index > -1) {
+ this.selectedUsernames.splice(index, 1);
+ } else {
+ this.selectedUsernames.push(username);
+ }
+
+ this.$emit('input', { ...this.value, data: '' });
},
fetchUsersBySearchTerm(search) {
return this.$apollo
@@ -79,6 +142,7 @@ export default {
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
this.users = Array.isArray(res) ? compact(res) : compact(res.data);
+ this.allUsers = this.allUsers.concat(this.users);
})
.catch(() =>
createAlert({
@@ -103,18 +167,32 @@ export default {
:get-active-token-value="getActiveUser"
:default-suggestions="defaultUsers"
:preloaded-suggestions="preloadedUsers"
+ :multi-select-values="selectedUsernames"
v-bind="$attrs"
@fetch-suggestions="fetchUsers"
+ @token-selected="handleSelected"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
- <gl-avatar
- v-if="activeTokenValue"
- :size="16"
- :src="getAvatarUrl(activeTokenValue)"
- class="gl-mr-2"
- />
- {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ <gl-intersperse v-if="multiSelectEnabled" separator=",">
+ <span
+ v-for="(username, index) in selectedUsernames"
+ :key="username"
+ :class="{ 'gl-ml-2': index > 0 }"
+ ><gl-avatar :size="16" :src="avatarFor(username)" class="gl-mr-1" />{{
+ displayNameFor(username)
+ }}</span
+ >
+ </gl-intersperse>
+ <template v-else>
+ <gl-avatar
+ v-if="activeTokenValue"
+ :size="16"
+ :src="getAvatarUrl(activeTokenValue)"
+ class="gl-mr-2"
+ />
+ {{ activeTokenValue ? activeTokenValue.name : inputValue }}
+ </template>
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
@@ -122,7 +200,15 @@ export default {
:key="user.username"
:value="user.username"
>
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex gl-align-items-center"
+ :class="{ 'gl-pl-6': addPadding(user.username) }"
+ >
+ <gl-icon
+ v-if="addCheckIcon(user.username)"
+ name="check"
+ class="gl-mr-3 gl-text-secondary gl-flex-shrink-0"
+ />
<gl-avatar :size="32" :src="getAvatarUrl(user)" />
<div>
<div>{{ user.name }}</div>
diff --git a/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js b/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js
new file mode 100644
index 00000000000..7c32e38a299
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/errors_alert.stories.js
@@ -0,0 +1,21 @@
+import ErrorsAlert from './errors_alert.vue';
+
+export default {
+ component: ErrorsAlert,
+ title: 'vue_shared/form/errors_alert',
+};
+
+const defaultProps = {
+ errors: ['Name must be at least 5 characters.', 'Name cannot contain special characters.'],
+};
+
+const Template = (args) => ({
+ components: { ErrorsAlert },
+ data() {
+ return { errors: args.errors };
+ },
+ template: `<errors-alert v-model="errors" />`,
+});
+
+export const Default = Template.bind({});
+Default.args = defaultProps;
diff --git a/app/assets/javascripts/vue_shared/components/form/errors_alert.vue b/app/assets/javascripts/vue_shared/components/form/errors_alert.vue
new file mode 100644
index 00000000000..3e33168781b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/form/errors_alert.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+export default {
+ components: { GlAlert },
+ model: {
+ prop: 'errors',
+ },
+ props: {
+ errors: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return n__(
+ 'The form contains the following error:',
+ 'The form contains the following errors:',
+ this.errors.length,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="errors.length"
+ class="gl-mb-5"
+ :title="title"
+ variant="danger"
+ @dismiss="$emit('input', [])"
+ >
+ <ul class="gl-pl-5 gl-mb-0">
+ <li v-for="error in errors" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+</template>
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 d97f1ae6135..0455685627d 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
@@ -94,8 +94,12 @@ export default {
computedValueIsVisible() {
return !this.showToggleVisibilityButton || this.valueIsVisible;
},
- inputType() {
- return this.computedValueIsVisible ? 'text' : 'password';
+ formInputClass() {
+ return [
+ 'gl-font-monospace! gl-cursor-default!',
+ { 'input-copy-show-disc': !this.computedValueIsVisible },
+ this.formInputGroupProps.class,
+ ];
},
},
mounted() {
@@ -157,10 +161,9 @@ export default {
ref="input"
:readonly="readonly"
:width="size"
- class="gl-font-monospace! gl-cursor-default!"
+ :class="formInputClass"
v-bind="formInputGroupProps"
:value="value"
- :type="inputType"
@input="handleInput"
@click="handleClick"
/>
@@ -194,3 +197,8 @@ export default {
</template>
</gl-form-group>
</template>
+<style>
+.input-copy-show-disc {
+ -webkit-text-security: disc;
+}
+</style>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
new file mode 100644
index 00000000000..cff9c56a1c0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
@@ -0,0 +1,6 @@
+import { __ } from '~/locale';
+
+export const CONFIG = {
+ users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true },
+ groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
new file mode 100644
index 00000000000..2d24cc5553b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/group_item.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'GroupItem',
+ components: {
+ GlAvatar,
+ GlButton,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ deleteButtonLabel() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
+ fullName() {
+ return this.data.fullName;
+ },
+ name() {
+ return this.data.name;
+ },
+ avatarUrl() {
+ return this.data.avatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', name)">
+ <gl-avatar :alt="fullName" :size="32" :src="avatarUrl" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ fullName }}</span>
+ <span class="gl-text-gray-600">@{{ name }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click="$emit('delete', name)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
new file mode 100644
index 00000000000..b8480a0c496
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
@@ -0,0 +1,193 @@
+<script>
+import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
+import Api from '~/api';
+import UserItem from './user_item.vue';
+import GroupItem from './group_item.vue';
+import { CONFIG } from './constants';
+
+const I18N = {
+ allGroups: __('All groups'),
+ projectGroups: __('Project groups'),
+ apiErrorMessage: __('An error occurred while fetching. Please try again.'),
+};
+
+export default {
+ name: 'ListSelector',
+ i18n: I18N,
+ components: {
+ GlCard,
+ GlIcon,
+ GlSearchBoxByType,
+ GlCollapsibleListbox,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ type: {
+ type: String,
+ required: true,
+ },
+ selectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ searchValue: '',
+ isProjectNamespace: 'true',
+ selected: [],
+ items: [],
+ };
+ },
+ computed: {
+ config() {
+ return CONFIG[this.type];
+ },
+ isUserVariant() {
+ return this.type === 'users';
+ },
+ component() {
+ return this.isUserVariant ? UserItem : GroupItem;
+ },
+ namespaceDropdownText() {
+ return parseBoolean(this.isProjectNamespace)
+ ? this.$options.i18n.projectGroups
+ : this.$options.i18n.allGroups;
+ },
+ },
+ methods: {
+ async handleSearchInput(search) {
+ this.$refs.results.open();
+
+ try {
+ if (this.isUserVariant) {
+ this.items = await this.fetchUsersBySearchTerm(search);
+ } else {
+ this.items = await this.fetchGroupsBySearchTerm(search);
+ }
+ } catch (e) {
+ createAlert({
+ message: this.$options.i18n.apiErrorMessage,
+ });
+ }
+ },
+ async fetchUsersBySearchTerm(search) {
+ let users = [];
+ if (parseBoolean(this.isProjectNamespace)) {
+ users = await Api.projectUsers(this.projectPath, search);
+ } else {
+ const groupMembers = await Api.groupMembers(this.groupPath, { query: search });
+ users = groupMembers?.data || [];
+ }
+
+ return users?.map((user) => ({ text: user.name, value: user.username, ...user }));
+ },
+ fetchGroupsBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: groupsAutocompleteQuery,
+ variables: { search },
+ })
+ .then(({ data }) =>
+ data?.groups.nodes.map((group) => ({
+ text: group.fullName,
+ value: group.name,
+ ...group,
+ })),
+ );
+ },
+ getItemByKey(key) {
+ return this.items.find((item) => item[this.config.filterKey] === key);
+ },
+ handleSelectItem(key) {
+ this.$emit('select', this.getItemByKey(key));
+ },
+ handleDeleteItem(key) {
+ this.$emit('delete', key);
+ },
+ handleSelectNamespace() {
+ this.items = [];
+ this.searchValue = '';
+ },
+ },
+ namespaceOptions: [
+ { text: I18N.projectGroups, value: 'true' },
+ { text: I18N.allGroups, value: 'false' },
+ ],
+};
+</script>
+
+<template>
+ <gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer">
+ <template #header
+ ><strong data-testid="list-selector-title"
+ >{{ title }}
+ <span class="gl-text-gray-700 gl-ml-3"
+ ><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span
+ ></strong
+ ></template
+ >
+
+ <div class="gl-display-flex gl-gap-3" :class="{ 'gl-mb-4': selectedItems.length }">
+ <gl-collapsible-listbox
+ ref="results"
+ v-model="selected"
+ class="list-selector gl-display-block gl-flex-grow-1"
+ :items="items"
+ multiple
+ @shown="$refs.search.focusInput()"
+ >
+ <template #toggle>
+ <gl-search-box-by-type
+ ref="search"
+ v-model="searchValue"
+ autofocus
+ debounce="500"
+ @input="handleSearchInput"
+ />
+ </template>
+
+ <template #list-item="{ item }">
+ <component :is="component" :data="item" @select="handleSelectItem" />
+ </template>
+ </gl-collapsible-listbox>
+
+ <gl-collapsible-listbox
+ v-if="config.showNamespaceDropdown"
+ v-model="isProjectNamespace"
+ :toggle-text="namespaceDropdownText"
+ :items="$options.namespaceOptions"
+ @select="handleSelectNamespace"
+ />
+ </div>
+
+ <component
+ :is="component"
+ v-for="(item, index) of selectedItems"
+ :key="index"
+ :class="{ 'gl-border-t': index > 0 }"
+ class="gl-p-3"
+ :data="item"
+ can-delete
+ @delete="handleDeleteItem"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
new file mode 100644
index 00000000000..fdbc767db81
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/user_item.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'UserItem',
+ components: {
+ GlAvatar,
+ GlButton,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ deleteButtonLabel() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
+ name() {
+ return this.data.name;
+ },
+ username() {
+ return this.data.username;
+ },
+ avatarUrl() {
+ return this.data.avatarUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', username)">
+ <gl-avatar :alt="name" :size="32" :src="avatarUrl" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ name }}</span>
+ <span class="gl-text-gray-600">@{{ username }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click="$emit('delete', username)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 741bdfd211b..cc3c95a047b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -492,7 +492,7 @@ export default {
tracking-property="quickAction"
/>
<comment-templates-dropdown
- v-if="!previewMarkdown && newCommentTemplatePath && glFeatures.savedReplies"
+ v-if="!previewMarkdown && newCommentTemplatePath"
:new-comment-template-path="newCommentTemplatePath"
@select="insertSavedReply"
/>
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 4a3c3cf0053..73c030b23dc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -190,7 +190,7 @@ export default {
renderMarkdown(markdown) {
const url = setUrlParams(
{ render_quick_actions: this.supportsQuickActions },
- joinPaths(gon.relative_url_root || window.location.origin, this.renderMarkdownPath),
+ joinPaths(window.location.origin, gon.relative_url_root, this.renderMarkdownPath),
);
return axios.post(url, { text: markdown }).then(({ data }) => data.body);
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
index 27237f2f16b..6d74c1d083a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -1,4 +1,4 @@
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 0ec8b6e2a0a..3bee539688b 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -64,7 +64,7 @@ export default {
});
},
lockedContextText() {
- return sprintf(__('This %{noteableTypeText} is locked.'), {
+ return sprintf(__('The discussion in this %{noteableTypeText} is locked.'), {
noteableTypeText: this.noteableTypeText,
});
},
@@ -80,7 +80,7 @@ export default {
<gl-sprintf
:message="
__(
- 'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and %{lockedLinkStart}locked%{lockedLinkEnd}.',
+ 'This %{noteableTypeText} is %{confidentialLinkStart}confidential%{confidentialLinkEnd} and its %{lockedLinkStart}discussion is locked%{lockedLinkEnd}.',
)
"
>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 81cbbf951ad..6a5884e4857 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -30,12 +30,10 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
-const MR_ICON_COLORS = {
+const ICON_COLORS = {
check: 'gl-bg-green-100 gl-text-green-700',
'merge-request-close': 'gl-bg-red-100 gl-text-red-700',
merge: 'gl-bg-blue-100 gl-text-blue-700',
-};
-const ICON_COLORS = {
'issue-close': 'gl-bg-blue-100 gl-text-blue-700',
};
@@ -76,6 +74,9 @@ export default {
noteAnchorId() {
return `note_${this.note.id}`;
},
+ isAllowedIcon() {
+ return Object.keys(ICON_COLORS).includes(this.note.system_note_icon_name);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -95,15 +96,8 @@ export default {
isMergeRequest() {
return this.getNoteableData.noteableType === 'MergeRequest';
},
- hasIconColors() {
- if (!this.isMergeRequest) return true;
-
- return this.isMergeRequest && MR_ICON_COLORS[this.note.system_note_icon_name];
- },
iconBgClass() {
- const colors = this.isMergeRequest ? MR_ICON_COLORS : ICON_COLORS;
-
- return colors[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
+ return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
},
},
mounted() {
@@ -140,17 +134,16 @@ export default {
:class="[
iconBgClass,
{
- 'mr-system-note-empty gl-bg-gray-900!': !hasIconColors,
- 'gl-w-6 gl-h-6 gl-mt-n1 gl-ml-2': !isMergeRequest,
- 'mr-system-note-icon': isMergeRequest,
+ 'system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
},
]"
class="gl-float-left gl--flex-center gl-rounded-full gl-relative timeline-icon"
>
<gl-icon
- v-if="note.system_note_icon_name && hasIconColors"
+ v-if="isAllowedIcon"
:name="note.system_note_icon_name"
- :size="isMergeRequest ? 12 : 16"
+ :size="12"
data-testid="timeline-icon"
/>
</div>
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
index 9bce9402afa..e2fd4477f0a 100644
--- 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
@@ -2,7 +2,6 @@
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',
@@ -14,25 +13,11 @@ export default {
SafeHtml,
},
props: {
- blameData: {
+ blameInfo: {
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>
@@ -41,10 +26,11 @@ export default {
<commit-info
v-for="(blame, index) in blameInfo"
:key="index"
- :class="{ 'gl-border-t': index !== 0 }"
+ :class="{ 'gl-border-t': blame.blameOffset !== '0px' }"
class="gl-display-flex gl-absolute gl-px-3"
:style="{ top: blame.blameOffset }"
:commit="blame.commit"
+ :prev-blame-link="blame.commitData && blame.commitData.projectBlameLink"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index 8dac6327a99..3b6dcace8fe 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -56,7 +56,6 @@ export default {
data() {
return {
hasAppeared: false,
- isLoading: true,
};
},
computed: {
@@ -68,17 +67,6 @@ export default {
return getPageSearchString(this.blamePath, page);
},
},
- created() {
- if (this.chunkIndex === 0) {
- // Display first chunk ASAP in order to improve perceived performance
- this.isLoading = false;
- return;
- }
-
- window.requestIdleCallback(() => {
- this.isLoading = false;
- });
- },
methods: {
handleChunkAppear() {
this.hasAppeared = true;
@@ -91,37 +79,37 @@ export default {
};
</script>
<template>
- <gl-intersection-observer @appear="handleChunkAppear">
- <div class="gl-display-flex">
- <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
- <div
- v-for="(n, index) in totalLines"
- :key="index"
- data-testid="line-numbers"
- class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ <div class="gl-display-flex">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div
+ v-for="(n, index) in totalLines"
+ :key="index"
+ data-testid="line-numbers"
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ >
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
+ ></a>
+ <a
+ :id="`L${calculateLineNumber(index)}`"
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ :href="`#L${calculateLineNumber(index)}`"
+ :data-line-number="calculateLineNumber(index)"
>
- <a
- class="gl-user-select-none gl-shadow-none! file-line-blame"
- :href="`${blamePath}${pageSearchString}#L${calculateLineNumber(index)}`"
- ></a>
- <a
- :id="`L${calculateLineNumber(index)}`"
- class="gl-user-select-none gl-shadow-none! file-line-num"
- :href="`#L${calculateLineNumber(index)}`"
- :data-line-number="calculateLineNumber(index)"
- >
- {{ calculateLineNumber(index) }}
- </a>
- </div>
+ {{ calculateLineNumber(index) }}
+ </a>
</div>
+ </div>
- <div v-else-if="!isLoading" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
- <!-- Placeholder for line numbers while content is not highlighted -->
- </div>
+ <div v-else class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <!-- Placeholder for line numbers while content is not highlighted -->
+ </div>
+ <gl-intersection-observer class="gl-w-full" @appear="handleChunkAppear">
<pre
class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
- ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else-if="!isLoading" v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
- </div>
- </gl-intersection-observer>
+ ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
+ </gl-intersection-observer>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
new file mode 100644
index 00000000000..a5f3f348cfc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
@@ -0,0 +1,36 @@
+#import "~/graphql_shared/fragments/author.fragment.graphql"
+
+query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: Int) {
+ project(fullPath: $fullPath) {
+ id
+ repository {
+ blobs(paths: [$filePath]) {
+ nodes {
+ id
+ blame(fromLine: $fromLine, toLine: $toLine) {
+ firstLine
+ groups {
+ lineno
+ span
+ commit {
+ id
+ titleHtml
+ message
+ authoredDate
+ authorGravatar
+ webPath
+ author {
+ ...Author
+ }
+ sha
+ }
+ commitData {
+ projectBlameLink
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index c7353ed6785..dcefa66c403 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -1,10 +1,15 @@
<script>
+import { debounce } from 'lodash';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import LineHighlighter from '~/blob/line_highlighter';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
import Chunk from './components/chunk_new.vue';
+import Blame from './components/blame_info.vue';
+import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
+import blameDataQuery from './queries/blame_data.query.graphql';
/*
* Note, this is a new experimental version of the SourceViewer, it is not ready for production use.
@@ -15,6 +20,7 @@ export default {
name: 'SourceViewerNew',
components: {
Chunk,
+ Blame,
},
directives: {
SafeHtml,
@@ -30,13 +36,55 @@ export default {
required: false,
default: () => [],
},
+ showBlame: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
lineHighlighter: new LineHighlighter(),
+ blameData: [],
+ renderedChunks: [],
};
},
+ computed: {
+ blameInfo() {
+ return this.blameData.reduce((result, blame, index) => {
+ if (shouldRender(this.blameData, index)) {
+ result.push({
+ ...blame,
+ blameOffset: calculateBlameOffset(blame.lineno, index),
+ });
+ }
+
+ return result;
+ }, []);
+ },
+ },
+ watch: {
+ showBlame: {
+ handler(shouldShow) {
+ toggleBlameClasses(this.blameData, shouldShow);
+ this.requestBlameInfo(this.renderedChunks[0]);
+ },
+ immediate: true,
+ },
+ blameData: {
+ handler(blameData) {
+ if (!this.showBlame) return;
+ toggleBlameClasses(blameData, true);
+ },
+ immediate: true,
+ },
+ },
created() {
+ this.handleAppear = debounce(this.handleChunkAppear, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
this.track(EVENT_ACTION, { label: EVENT_LABEL_VIEWER, property: this.blob.language });
addBlobLinksTracking();
},
@@ -44,10 +92,39 @@ export default {
this.selectLine();
},
methods: {
+ async handleChunkAppear(chunkIndex, handleOverlappingChunk = true) {
+ if (!this.renderedChunks.includes(chunkIndex)) {
+ this.renderedChunks.push(chunkIndex);
+ await this.requestBlameInfo(chunkIndex);
+
+ if (chunkIndex > 0 && handleOverlappingChunk) {
+ // request the blame information for overlapping chunk incase it is visible in the DOM
+ this.handleChunkAppear(chunkIndex - 1, false);
+ }
+ }
+ },
+ async requestBlameInfo(chunkIndex) {
+ const chunk = this.chunks[chunkIndex];
+ if (!this.showBlame || !chunk) return;
+
+ const { data } = await this.$apollo.query({
+ query: blameDataQuery,
+ variables: {
+ fullPath: this.projectPath,
+ filePath: this.blob.path,
+ fromLine: chunk.startingFrom + 1,
+ toLine: chunk.startingFrom + chunk.totalLines,
+ },
+ });
+
+ const blob = data?.project?.repository?.blobs?.nodes[0];
+ const blameGroups = blob?.blame?.groups;
+ const isDuplicate = this.blameData.includes(blameGroups[0]);
+ if (blameGroups && !isDuplicate) this.blameData.push(...blameGroups);
+ },
async selectLine() {
await this.$nextTick();
- const scrollEnabled = false;
- this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ this.lineHighlighter.highlightHash(this.$route.hash);
},
},
userColorScheme: window.gon.user_color_scheme,
@@ -55,24 +132,27 @@ export default {
</script>
<template>
- <div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto"
- :class="$options.userColorScheme"
- data-type="simple"
- :data-path="blob.path"
- data-qa-selector="blob_viewer_file_content"
- >
- <chunk
- v-for="(chunk, _, index) in chunks"
- :key="index"
- :chunk-index="index"
- :is-highlighted="Boolean(chunk.isHighlighted)"
- :raw-content="chunk.rawContent"
- :highlighted-content="chunk.highlightedContent"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :blame-path="blob.blamePath"
- @appear="selectLine"
- />
+ <div class="gl-display-flex">
+ <blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
+
+ <div
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full"
+ :class="$options.userColorScheme"
+ data-type="simple"
+ :data-path="blob.path"
+ >
+ <chunk
+ v-for="(chunk, index) in chunks"
+ :key="index"
+ :chunk-index="index"
+ :is-highlighted="Boolean(chunk.isHighlighted)"
+ :raw-content="chunk.rawContent"
+ :highlighted-content="chunk.highlightedContent"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :blame-path="blob.blamePath"
+ @appear="() => handleAppear(index)"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
index af01653fc0d..596829b51a4 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/utils.js
@@ -1,6 +1,7 @@
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 VIEWER_SELECTOR = '.file-holder .blob-viewer';
const findLineNumberElement = (lineNumber) => document.getElementById(`L${lineNumber}`);
@@ -8,8 +9,18 @@ const findLineContentElement = (lineNumber) => document.getElementById(`LC${line
export const calculateBlameOffset = (lineNumber) => {
if (lineNumber === 1) return '0px';
- const lineContentOffset = findLineContentElement(lineNumber)?.offsetTop;
- return `${lineContentOffset}px`;
+ const blobViewerOffset = document.querySelector(VIEWER_SELECTOR)?.getBoundingClientRect().top;
+ const lineContentOffset = findLineContentElement(lineNumber)?.getBoundingClientRect().top;
+ return `${lineContentOffset - blobViewerOffset}px`;
+};
+
+export const shouldRender = (data, index) => {
+ const prevBlame = data[index - 1];
+ const currBlame = data[index];
+ const identicalSha = currBlame.commit.sha === prevBlame?.commit?.sha;
+ const lineNumberSmaller = currBlame.lineno < prevBlame?.lineno;
+
+ return !identicalSha || lineNumberSmaller;
};
export const toggleBlameClasses = (blameData, isVisible) => {
@@ -17,7 +28,9 @@ 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 }) => {
+ blameData.forEach(({ lineno, span }, index) => {
+ if (!shouldRender(blameData, index)) return;
+
const lineNumberEl = findLineNumberElement(lineno)?.parentElement;
const lineContentEl = findLineContentElement(lineno);
const lineNumberSpanEl = findLineNumberElement(lineno + span - 1)?.parentElement;
diff --git a/app/assets/javascripts/vue_shared/components/toggle_labels.vue b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
index 05c837e32f0..db20e1288aa 100644
--- a/app/assets/javascripts/vue_shared/components/toggle_labels.vue
+++ b/app/assets/javascripts/vue_shared/components/toggle_labels.vue
@@ -54,7 +54,6 @@ export default {
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"
/>
diff --git a/app/assets/javascripts/vue_shared/components/users_table/constants.js b/app/assets/javascripts/vue_shared/components/users_table/constants.js
new file mode 100644
index 00000000000..2a063a1be33
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/users_table/constants.js
@@ -0,0 +1,3 @@
+export const USER_AVATAR_SIZE = 32;
+
+export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
diff --git a/app/assets/javascripts/admin/users/components/user_avatar.vue b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
index dd354794cf3..5d86f90880d 100644
--- a/app/assets/javascripts/admin/users/components/user_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
@@ -1,7 +1,7 @@
<script>
import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { truncate } from '~/lib/utils/text_utility';
-import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from '../constants';
+import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from './constants';
export default {
directives: {
@@ -23,12 +23,21 @@ export default {
},
},
computed: {
+ subLabel() {
+ if (this.user.email) {
+ return {
+ label: this.user.email,
+ link: `mailto:${this.user.email}`,
+ };
+ }
+
+ return {
+ label: `@${this.user.username}`,
+ };
+ },
adminUserHref() {
return this.adminUserPath.replace('id', this.user.username);
},
- adminUserMailto() {
- return `mailto:${this.user.email}`;
- },
userNoteShort() {
return truncate(this.user.note, LENGTH_OF_USER_NOTE_TOOLTIP);
},
@@ -48,9 +57,9 @@ export default {
:size="$options.USER_AVATAR_SIZE"
:src="user.avatarUrl"
:label="user.name"
- :sub-label="user.email"
+ :sub-label="subLabel.label"
:label-link="adminUserHref"
- :sub-label-link="adminUserMailto"
+ :sub-label-link="subLabel.link"
>
<template #meta>
<div v-if="user.note" class="gl-text-gray-500 gl-p-1">
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
index 65737be1e67..be164bb07a3 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/users_table.vue
@@ -1,12 +1,8 @@
<script>
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
import { thWidthPercent } from '~/lib/utils/table_utility';
-import { s__, __ } from '~/locale';
+import { __ } from '~/locale';
import UserDate from '~/vue_shared/components/user_date.vue';
-import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql';
-import UserActions from './user_actions.vue';
import UserAvatar from './user_avatar.vue';
export default {
@@ -14,7 +10,6 @@ export default {
GlSkeletonLoader,
GlTable,
UserAvatar,
- UserActions,
UserDate,
},
props: {
@@ -22,49 +17,20 @@ export default {
type: Array,
required: true,
},
- paths: {
- type: Object,
+ adminUserPath: {
+ type: String,
required: true,
},
- },
- data() {
- return {
- groupCounts: [],
- };
- },
- apollo: {
groupCounts: {
- query: getUsersGroupCountsQuery,
- variables() {
- return {
- usernames: this.users.map((user) => user.username),
- };
- },
- update(data) {
- const nodes = data?.users?.nodes || [];
- const parsedIds = convertNodeIdsFromGraphQLIds(nodes);
-
- return parsedIds.reduce((acc, { id, groupCount }) => {
- acc[id] = groupCount || 0;
- return acc;
- }, {});
- },
- error(error) {
- createAlert({
- message: this.$options.i18n.groupCountFetchError,
- captureError: true,
- error,
- });
- },
- skip() {
- return !this.users.length;
- },
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ groupCountsLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- },
- i18n: {
- groupCountFetchError: s__(
- 'AdminUsers|Could not load user group counts. Please refresh the page to try again.',
- ),
},
fields: [
{
@@ -112,7 +78,7 @@ export default {
:tbody-tr-attr="{ 'data-testid': 'user-row-content' }"
>
<template #cell(name)="{ item: user }">
- <user-avatar :user="user" :admin-user-path="paths.adminUser" />
+ <user-avatar :user="user" :admin-user-path="adminUserPath" />
</template>
<template #cell(createdAt)="{ item: { createdAt } }">
@@ -125,17 +91,19 @@ export default {
<template #cell(groupCount)="{ item: { id } }">
<div :data-testid="`user-group-count-${id}`">
- <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" />
- <span v-else>{{ groupCounts[id] }}</span>
+ <gl-skeleton-loader v-if="groupCountsLoading" :width="40" :lines="1" />
+ <span v-else>{{ groupCounts[id] || 0 }}</span>
</div>
</template>
<template #cell(projectsCount)="{ item: { id, projectsCount } }">
- <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div>
+ <div :data-testid="`user-project-count-${id}`">
+ {{ projectsCount || 0 }}
+ </div>
</template>
<template #cell(settings)="{ item: user }">
- <user-actions :user="user" :paths="paths" :show-button-labels="true" />
+ <slot name="user-actions" :user="user"></slot>
</template>
</gl-table>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 9fb0add5522..441b4c31b3a 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -335,7 +335,7 @@ export default {
:variant="isBlob ? 'confirm' : 'default'"
:category="isBlob ? 'primary' : 'secondary'"
:toggle-text="$options.i18n.toggleText"
- data-qa-selector="action_dropdown"
+ data-testid="action-dropdown"
fluid-width
block
@shown="$emit('shown')"
@@ -347,7 +347,7 @@ export default {
v-for="action in actions"
:key="action.key"
:item="action"
- :data-qa-selector="`${action.key}_menu_item`"
+ :data-testid="`${action.key}-menu-item`"
@action="executeAction(action)"
>
<template #list-item>
diff --git a/app/assets/javascripts/vue_shared/directives/safe_html.js b/app/assets/javascripts/vue_shared/directives/safe_html.js
index 450c7fc1bc5..c731f742771 100644
--- a/app/assets/javascripts/vue_shared/directives/safe_html.js
+++ b/app/assets/javascripts/vue_shared/directives/safe_html.js
@@ -11,7 +11,7 @@ const DEFAULT_CONFIG = {
const transform = (el, binding) => {
if (binding.oldValue !== binding.value) {
- const config = { ...DEFAULT_CONFIG, ...(binding.arg ?? {}) };
+ const config = { ...DEFAULT_CONFIG, ...binding.arg };
el.textContent = '';
diff --git a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
index 79946ebaecd..a1abb079cc2 100644
--- a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
+++ b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js
@@ -2,12 +2,11 @@ export default (Vue) => {
Vue.mixin({
provide() {
return {
- glFeatures:
- {
- ...window.gon?.features,
- // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460
- ...window.gon?.licensed_features,
- } || {},
+ glFeatures: {
+ ...window.gon?.features,
+ // TODO: extract into glLicensedFeatures https://gitlab.com/gitlab-org/gitlab/-/issues/322460
+ ...window.gon?.licensed_features,
+ },
};
},
});
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index 45fde45f516..dae3ddfe016 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: 0,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
isUpdated() {
@@ -161,6 +166,7 @@ export default {
:issuable="issuable"
:status-icon="statusIcon"
:enable-edit="enableEdit"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
>
<template #status-badge>
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 a9b5e3a66a8..62a2b44e660 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
@@ -221,7 +221,7 @@ export default {
@click="handleRightSidebarToggleClick"
/>
</div>
- <div class="detail-page-header-actions gl-display-flex">
+ <div class="detail-page-header-actions gl-align-self-center gl-display-flex">
<slot name="header-actions"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 3878c16c8d0..040f49c7c25 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -147,6 +147,7 @@ export default {
:description-help-path="descriptionHelpPath"
:task-list-update-path="taskListUpdatePath"
:task-list-lock-version="taskListLockVersion"
+ :workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
@task-list-update-success="$emit('task-list-update-success', $event)"
@task-list-update-failure="$emit('task-list-update-failure')"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index da71adc8abd..5387e39e3eb 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -1,5 +1,6 @@
<script>
import { GlIcon, GlBadge, GlButton, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { STATUS_OPEN } from '~/issues/constants';
import { __ } from '~/locale';
@@ -13,6 +14,7 @@ export default {
GlBadge,
GlButton,
GlIntersectionObserver,
+ ConfidentialityBadge,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,6 +33,11 @@ export default {
type: Boolean,
required: true,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -79,9 +86,7 @@ export default {
class="issue-sticky-header gl-fixed gl-z-index-3 gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-py-3"
data-testid="header"
>
- <div
- class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto gl-px-5"
- >
+ <div class="issue-sticky-header-text gl-display-flex gl-align-items-baseline gl-mx-auto">
<gl-badge
class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
:variant="badgeVariant"
@@ -91,6 +96,12 @@ export default {
<slot name="status-badge"></slot>
</span>
</gl-badge>
+ <confidentiality-badge
+ v-if="issuable.confidential"
+ class="gl-white-space-nowrap gl-mr-3 gl-align-self-center"
+ :issuable-type="issuable.type"
+ :workspace-type="workspaceType"
+ />
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="issuable.title"
diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue
index 91d7e21500a..b5e0a4b2348 100644
--- a/app/assets/javascripts/webhooks/components/push_events.vue
+++ b/app/assets/javascripts/webhooks/components/push_events.vue
@@ -43,7 +43,7 @@ export default {
value="all_branches"
data-testid="rule_all_branches"
>
- <div data-qa-selector="strategy_radio_all">{{ __('All branches') }}</div>
+ <div>{{ __('All branches') }}</div>
</gl-form-radio>
<!-- wildcard -->
@@ -52,7 +52,7 @@ export default {
value="wildcard"
data-testid="rule_wildcard"
>
- <div data-qa-selector="strategy_radio_wildcard">
+ <div>
{{ s__('Webhooks|Wildcard pattern') }}
</div>
</gl-form-radio>
@@ -61,7 +61,6 @@ export default {
v-if="branchFilterStrategyData === 'wildcard'"
v-model="pushEventsBranchFilterData"
name="hook[push_events_branch_filter]"
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
/>
</div>
@@ -85,7 +84,7 @@ export default {
value="regex"
data-testid="rule_regex"
>
- <div data-qa-selector="strategy_radio_regex">
+ <div>
{{ s__('Webhooks|Regular expression') }}
</div>
</gl-form-radio>
@@ -94,7 +93,6 @@ export default {
v-if="branchFilterStrategyData === 'regex'"
v-model="pushEventsBranchFilterData"
name="hook[push_events_branch_filter]"
- data-qa-selector="webhook_branch_filter_field"
data-testid="webhook_branch_filter_field"
/>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
index 7903adea9bd..31cfe387b6e 100644
--- a/app/assets/javascripts/work_items/components/notes/system_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -26,6 +26,11 @@ import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+const ALLOWED_ICONS = ['issue-close'];
+const ICON_COLORS = {
+ 'issue-close': 'gl-bg-blue-100! gl-text-blue-700',
+};
+
export default {
i18n: {
deleteButtonLabel: __('Remove description history'),
@@ -66,6 +71,12 @@ export default {
noteAnchorId() {
return `note_${this.noteId}`;
},
+ getIconColor() {
+ return ICON_COLORS[this.note.systemNoteIconName] || '';
+ },
+ isAllowedIcon() {
+ return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
+ },
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
},
@@ -102,9 +113,16 @@ export default {
class="note system-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"
+ :class="[
+ getIconColor,
+ {
+ 'gl-bg-gray-50 gl-text-gray-600 system-note-icon': isAllowedIcon,
+ 'system-note-tiny-dot gl-bg-gray-900!': !isAllowedIcon,
+ },
+ ]"
+ class="gl-float-left gl--flex-center gl-rounded-full gl-relative"
>
- <gl-icon :name="note.systemNoteIconName" />
+ <gl-icon v-if="isAllowedIcon" :size="12" :name="note.systemNoteIconName" />
</div>
<div class="timeline-content">
<div class="note-header">
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 c867e53dc30..c3b7b7a2953 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
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { ASC } from '~/notes/constants';
import { __ } from '~/locale';
@@ -105,7 +105,7 @@ export default {
};
},
update(data) {
- return data.workspace.workItems.nodes[0];
+ return data.workspace.workItems.nodes[0] ?? {};
},
skip() {
return !this.workItemIid;
@@ -150,13 +150,13 @@ export default {
};
},
isProjectArchived() {
- return this.workItem?.project?.archived;
+ return this.workItem.archived;
},
canCreateNote() {
- return this.workItem?.userPermissions?.createNote;
+ return this.workItem.userPermissions?.createNote;
},
workItemState() {
- return this.workItem?.state;
+ return this.workItem.state;
},
commentButtonText() {
return this.isNewDiscussion ? __('Comment') : __('Reply');
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 c7d8a50f402..1e6bd9ff1ac 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
@@ -8,7 +8,7 @@ import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import WorkItemStateToggleButton from '~/work_items/components/work_item_state_toggle_button.vue';
+import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
@@ -29,7 +29,7 @@ export default {
MarkdownEditor,
GlFormCheckbox,
GlIcon,
- WorkItemStateToggleButton,
+ WorkItemStateToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -195,7 +195,6 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:form-field-props="formFieldProps"
:add-spacing-classes="false"
- data-testid="work-item-add-comment"
use-bottom-toolbar
supports-quick-actions
:autofocus="autofocus"
@@ -230,7 +229,7 @@ export default {
@click="$emit('submitForm', { commentText, isNoteInternal })"
>{{ commentButtonTextComputed }}
</gl-button>
- <work-item-state-toggle-button
+ <work-item-state-toggle
v-if="isNewDiscussion"
class="gl-ml-3"
:work-item-id="workItemId"
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 f4c654f054c..11aecc65803 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
@@ -1,6 +1,6 @@
<script>
import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import Tracking from '~/tracking';
@@ -96,6 +96,7 @@ export default {
data() {
return {
isEditing: false,
+ workItem: {},
};
},
computed: {
@@ -163,13 +164,13 @@ export default {
return this.authorId === this.currentUserId;
},
isWorkItemAuthor() {
- return getIdFromGraphQLId(this.workItem?.author?.id) === this.authorId;
+ return getIdFromGraphQLId(this.workItem.author?.id) === this.authorId;
},
projectName() {
- return this.workItem?.project?.name;
+ return this.workItem.namespace?.name;
},
isWorkItemConfidential() {
- return this.workItem?.confidential;
+ return this.workItem.confidential;
},
},
apollo: {
@@ -184,7 +185,7 @@ export default {
};
},
update(data) {
- return data.workspace?.workItems?.nodes[0];
+ return data.workspace?.workItems?.nodes[0] ?? {};
},
skip() {
return !this.workItemIid;
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 2cdf8b5ea9d..cb9a560f9e1 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
@@ -5,7 +5,7 @@ import {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, sprintf } from '~/locale';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
@@ -207,7 +207,6 @@ export default {
<gl-button
v-if="showEdit"
v-gl-tooltip
- data-testid="edit-work-item-note"
data-track-action="click_button"
data-track-label="edit_button"
category="tertiary"
@@ -219,7 +218,6 @@ export default {
<gl-disclosure-dropdown
ref="dropdown"
v-gl-tooltip
- data-testid="work-item-note-actions"
icon="ellipsis_v"
text-sr-only
placement="right"
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 17d22e66530..75a8a7b29c0 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
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { getMutation, optimisticAwardUpdate } from '../../notes/award_utils';
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
index bccbec903b4..e073fddeddb 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_signed_out.vue
@@ -27,5 +27,8 @@ export default {
</script>
<template>
- <div v-safe-html="signedOutText" class="disabled-comment gl-text-center gl-relative"></div>
+ <div
+ v-safe-html="signedOutText"
+ class="disabled-comment gl-text-center gl-text-secondary gl-relative"
+ ></div>
</template>
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 49813edf6fc..cbe7de4abcd 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
@@ -1,6 +1,6 @@
<script>
-import { GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { GlLabel, GlLink, GlIcon, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
@@ -15,21 +15,21 @@ import {
WIDGET_TYPE_LABELS,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
-import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
i18n: {
confidential: __('Confidential'),
created: __('Created'),
closed: __('Closed'),
+ remove: s__('WorkItem|Remove'),
},
components: {
GlLabel,
GlLink,
GlIcon,
+ GlButton,
RichTimestampTooltip,
WorkItemLinkChildMetadata,
- WorkItemLinksMenu,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -52,6 +52,16 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ isFocused: false,
+ };
},
computed: {
labels() {
@@ -106,6 +116,12 @@ export default {
}
return false;
},
+ showRemove() {
+ return this.canUpdate && this.isFocused;
+ },
+ displayLabels() {
+ return this.showLabels && this.labels.length;
+ },
},
methods: {
showScopedLabel(label) {
@@ -117,8 +133,12 @@ export default {
<template>
<div
- class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
+ class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base gl-gap-3"
data-testid="links-child"
+ @mouseover="isFocused = true"
+ @mouseleave="isFocused = false"
+ @focusin="isFocused = true"
+ @focusout="isFocused = false"
>
<div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
<div
@@ -168,7 +188,7 @@ export default {
class="gl-ml-6 ml-xl-0"
/>
</div>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
+ <div v-if="displayLabels" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
<gl-label
v-for="label in labels"
:key="label.id"
@@ -181,10 +201,16 @@ export default {
/>
</div>
</div>
- <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
- <work-item-links-menu
- data-testid="links-menu"
- @removeChild="$emit('removeChild', childItem)"
+ <div v-if="canUpdate">
+ <gl-button
+ :class="{ 'gl-visibility-visible': showRemove }"
+ class="gl-visibility-hidden"
+ category="tertiary"
+ size="small"
+ icon="close"
+ :aria-label="$options.i18n.remove"
+ data-testid="remove-work-item-link"
+ @click="$emit('removeChild', childItem)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
deleted file mode 100644
index 12b7bade31d..00000000000
--- a/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-
-export default {
- components: {
- GlDisclosureDropdownItem,
- GlDisclosureDropdown,
- },
-};
-</script>
-
-<template>
- <div class="gl-ml-5">
- <gl-disclosure-dropdown
- category="tertiary"
- toggle-class="btn-icon btn-sm"
- icon="ellipsis_v"
- data-testid="work_items_links_menu"
- :aria-label="__(`More actions`)"
- text-sr-only
- no-caret
- >
- <gl-disclosure-dropdown-item @action="$emit('removeChild')">
- <template #list-item>{{ s__('WorkItem|Remove') }}</template>
- </gl-disclosure-dropdown-item>
- </gl-disclosure-dropdown>
- </div>
-</template>
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 3595ab631df..c122db6c902 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
@@ -1,20 +1,29 @@
<script>
-import { GlTokenSelector } from '@gitlab/ui';
+import { GlTokenSelector, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
+
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { isNumeric } from '~/lib/utils/number_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { highlighter } from 'ee_else_ce/gfm_auto_complete';
+import groupWorkItemsQuery from '../../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ I18N_WORK_ITEM_SEARCH_ERROR,
sprintfWorkItem,
} from '../../constants';
export default {
components: {
GlTokenSelector,
+ GlAlert,
},
+ directives: { SafeHtml },
+ inject: ['isGroup'],
props: {
value: {
type: Array,
@@ -47,30 +56,37 @@ export default {
},
apollo: {
availableWorkItems: {
- query: projectWorkItemsQuery,
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
+ searchTerm: '',
types: this.childrenType ? [this.childrenType] : [],
- in: this.search ? 'TITLE' : undefined,
+ isNumber: false,
};
},
skip() {
return !this.searchStarted;
},
update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
- );
+ return [
+ ...this.filterItems(data.workspace.workItemsByIid?.nodes),
+ ...this.filterItems(data.workspace.workItems.nodes),
+ ];
+ },
+ error() {
+ this.error = sprintfWorkItem(I18N_WORK_ITEM_SEARCH_ERROR, this.childrenTypeName);
},
},
},
data() {
return {
availableWorkItems: [],
- search: '',
+ query: '',
searchStarted: false,
+ error: '',
};
},
computed: {
@@ -101,7 +117,24 @@ export default {
methods: {
getIdFromGraphQLId,
setSearchKey(value) {
- this.search = value;
+ this.query = value;
+
+ // Query parameters for searching by text
+ const variables = {
+ searchTerm: value,
+ in: value ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
+ };
+
+ // Check if it is a number, add iid as query parameter
+ if (isNumeric(value) && value) {
+ variables.iid = value;
+ variables.isNumber = true;
+ }
+
+ // Fetch combined results of search by iid and search by title.
+ this.$apollo.queries.availableWorkItems.refetch(variables);
},
handleFocus() {
this.searchStarted = true;
@@ -125,33 +158,58 @@ export default {
}
});
},
+ formatResults(input) {
+ if (!this.query) {
+ return input;
+ }
+
+ return highlighter(`<span class="gl-text-black-normal">${input}</span>`, this.query);
+ },
+ unsetError() {
+ this.error = '';
+ },
+ filterItems(items) {
+ return (
+ items?.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
+ ) || []
+ );
+ },
},
};
</script>
<template>
- <gl-token-selector
- ref="tokenSelector"
- v-model="workItemsToAdd"
- :dropdown-items="availableWorkItems"
- :loading="isLoading"
- :placeholder="addInputPlaceholder"
- menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
- :container-class="tokenSelectorContainerClass"
- data-testid="work-item-token-select-input"
- @text-input="debouncedSearchKeyUpdate"
- @focus="handleFocus"
- @mouseover.native="handleMouseOver"
- @mouseout.native="handleMouseOut"
- @token-add="focusInputText"
- @token-remove="focusInputText"
- @blur="handleBlur"
- >
- <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-font-sm gl-mr-4">{{ dropdownItem.iid }}</div>
- <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
- </div>
- </template>
- </gl-token-selector>
+ <div>
+ <gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
+ {{ error }}
+ </gl-alert>
+ <gl-token-selector
+ ref="tokenSelector"
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="addInputPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ :container-class="tokenSelectorContainerClass"
+ data-testid="work-item-token-select-input"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ @token-add="focusInputText"
+ @token-remove="focusInputText"
+ @blur="handleBlur"
+ >
+ <template #token-content="{ token }"> {{ token.iid }} {{ token.title }} </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <div class="gl-display-flex">
+ <div
+ v-safe-html="formatResults(dropdownItem.iid)"
+ class="gl-text-secondary gl-font-sm gl-mr-4"
+ ></div>
+ <div v-safe-html="formatResults(dropdownItem.title)" class="gl-text-truncate"></div>
+ </div>
+ </template>
+ </gl-token-selector>
+ </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 02d2ea24ca0..0a71fbc9a34 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -8,7 +8,7 @@ import {
GlToggle,
} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -20,12 +20,12 @@ import {
I18N_WORK_ITEM_DELETE,
I18N_WORK_ITEM_ARE_YOU_SURE_DELETE,
TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
- TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
TEST_ID_DELETE_ACTION,
TEST_ID_PROMOTE_ACTION,
TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
TEST_ID_COPY_REFERENCE_ACTION,
+ TEST_ID_TOGGLE_ACTION,
I18N_WORK_ITEM_ERROR_CONVERTING,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
@@ -36,11 +36,12 @@ import {
import updateWorkItemNotificationsMutation from '../graphql/update_work_item_notifications.mutation.graphql';
import convertWorkItemMutation from '../graphql/work_item_convert.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import WorkItemStateToggle from './work_item_state_toggle.vue';
export default {
i18n: {
- enableTaskConfidentiality: s__('WorkItem|Turn on confidentiality'),
- disableTaskConfidentiality: s__('WorkItem|Turn off confidentiality'),
+ enableConfidentiality: s__('WorkItem|Turn on confidentiality'),
+ disableConfidentiality: s__('WorkItem|Turn off confidentiality'),
notifications: s__('WorkItem|Notifications'),
notificationOn: s__('WorkItem|Notifications turned on.'),
notificationOff: s__('WorkItem|Notifications turned off.'),
@@ -54,25 +55,30 @@ export default {
GlDropdownDivider,
GlModal,
GlToggle,
+ WorkItemStateToggle,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin({ label: 'actions_menu' })],
isLoggedIn: isLoggedIn(),
- notificationsToggleTestId: TEST_ID_NOTIFICATIONS_TOGGLE_ACTION,
notificationsToggleFormTestId: TEST_ID_NOTIFICATIONS_TOGGLE_FORM,
confidentialityTestId: TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION,
copyReferenceTestId: TEST_ID_COPY_REFERENCE_ACTION,
copyCreateNoteEmailTestId: TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION,
deleteActionTestId: TEST_ID_DELETE_ACTION,
promoteActionTestId: TEST_ID_PROMOTE_ACTION,
+ stateToggleTestId: TEST_ID_TOGGLE_ACTION,
inject: ['isGroup'],
props: {
fullPath: {
type: String,
required: true,
},
+ workItemState: {
+ type: String,
+ required: true,
+ },
workItemId: {
type: String,
required: false,
@@ -128,6 +134,11 @@ export default {
required: false,
default: false,
},
+ workItemParentId: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
workItemTypes: {
@@ -165,6 +176,11 @@ export default {
canPromoteToObjective() {
return this.canUpdate && this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT;
},
+ confidentialItemText() {
+ return this.isConfidential
+ ? this.$options.i18n.disableConfidentiality
+ : this.$options.i18n.enableConfidentiality;
+ },
objectiveWorkItemTypeId() {
return this.workItemTypes.find((type) => type.name === WORK_ITEM_TYPE_VALUE_OBJECTIVE).id;
},
@@ -267,7 +283,7 @@ export default {
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
- :text="__('More actions')"
+ :toggle-text="__('More actions')"
category="tertiary"
:auto-close="false"
no-caret
@@ -282,7 +298,6 @@ export default {
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
- :data-testid="$options.notificationsToggleTestId"
class="work-item-notification-toggle"
label-position="left"
@change="toggleNotifications($event)"
@@ -299,49 +314,56 @@ export default {
>
<template #list-item>{{ __('Promote to objective') }}</template>
</gl-disclosure-dropdown-item>
- <template v-if="canUpdate && !isParentConfidential">
- <gl-disclosure-dropdown-item
- :data-testid="$options.confidentialityTestId"
- @action="handleToggleWorkItemConfidentiality"
- ><template #list-item>{{
- isConfidential
- ? $options.i18n.disableTaskConfidentiality
- : $options.i18n.enableTaskConfidentiality
- }}</template></gl-disclosure-dropdown-item
- >
- </template>
+
+ <gl-disclosure-dropdown-item
+ v-if="canUpdate && !isParentConfidential"
+ :data-testid="$options.confidentialityTestId"
+ @action="handleToggleWorkItemConfidentiality"
+ >
+ <template #list-item>{{ confidentialItemText }}</template>
+ </gl-disclosure-dropdown-item>
+
+ <work-item-state-toggle
+ v-if="canUpdate"
+ :data-testid="$options.stateToggleTestId"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-parent-id="workItemParentId"
+ :work-item-type="workItemType"
+ show-as-dropdown-item
+ />
+
<gl-disclosure-dropdown-item
- ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
@action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
- ><template #list-item>{{
- $options.i18n.copyReference
- }}</template></gl-disclosure-dropdown-item
>
- <template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
- <gl-disclosure-dropdown-item
- ref="workItemCreateNoteEmail"
- :data-testid="$options.copyCreateNoteEmailTestId"
- :data-clipboard-text="workItemCreateNoteEmail"
- @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
- ><template #list-item>{{
- i18n.copyCreateNoteEmail
- }}</template></gl-disclosure-dropdown-item
- >
- </template>
- <gl-dropdown-divider v-if="canDelete" />
+ <template #list-item>{{ $options.i18n.copyReference }}</template>
+ </gl-disclosure-dropdown-item>
+
<gl-disclosure-dropdown-item
- v-if="canDelete"
- :data-testid="$options.deleteActionTestId"
- variant="danger"
- @action="handleDelete"
+ v-if="$options.isLoggedIn && workItemCreateNoteEmail"
+ :data-testid="$options.copyCreateNoteEmailTestId"
+ :data-clipboard-text="workItemCreateNoteEmail"
+ @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
>
- <template #list-item
- ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
- >
+ <template #list-item>{{ i18n.copyCreateNoteEmail }}</template>
</gl-disclosure-dropdown-item>
+
+ <template v-if="canDelete">
+ <gl-dropdown-divider />
+ <gl-disclosure-dropdown-item
+ :data-testid="$options.deleteActionTestId"
+ variant="danger"
+ @action="handleDelete"
+ >
+ <template #list-item>
+ <span class="text-danger">{{ i18n.deleteWorkItem }}</span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </template>
</gl-disclosure-dropdown>
+
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
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 fd01d855782..7d09a003926 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
@@ -13,6 +13,7 @@ import {
WIDGET_TYPE_WEIGHT,
WORK_ITEM_TYPE_VALUE_KEY_RESULT,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WORK_ITEM_TYPE_VALUE_TASK,
} from '../constants';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
@@ -98,7 +99,8 @@ export default {
showWorkItemParent() {
return (
this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE ||
- this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT
+ this.workItemType === WORK_ITEM_TYPE_VALUE_KEY_RESULT ||
+ this.workItemType === WORK_ITEM_TYPE_VALUE_TASK
);
},
workItemParent() {
diff --git a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
index 44bd17b59a2..f806946509f 100644
--- a/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
+++ b/app/assets/javascripts/work_items/components/work_item_award_emoji.vue
@@ -1,13 +1,14 @@
<script>
-import * as Sentry from '@sentry/browser';
import { produce } from 'immer';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
-import workItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
+import groupWorkItemAwardEmojiQuery from '../graphql/group_award_emoji.query.graphql';
+import projectWorkItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql';
import {
EMOJI_THUMBSDOWN,
@@ -23,6 +24,7 @@ export default {
components: {
AwardsList,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -75,7 +77,9 @@ export default {
},
apollo: {
awardEmoji: {
- query: workItemAwardEmojiQuery,
+ query() {
+ return this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery;
+ },
variables() {
return {
iid: this.workItemIid,
@@ -116,7 +120,7 @@ export default {
after: this.pageInfo?.endCursor,
},
});
- } catch (error) {
+ } catch {
this.$emit('error', I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR);
}
},
@@ -139,7 +143,7 @@ export default {
return this.awardEmoji.nodes;
}
- // else make a copy of unmutable list and return the list after adding the new emoji
+ // else make a copy of immutable list and return the list after adding the new emoji
const awardEmojiNodes = [...this.awardEmoji.nodes];
awardEmojiNodes.push({
name,
@@ -162,7 +166,7 @@ export default {
},
updateWorkItemAwardEmojiWidgetCache({ cache, name, toggledOn }) {
const query = {
- query: workItemAwardEmojiQuery,
+ query: this.isGroup ? groupWorkItemAwardEmojiQuery : projectWorkItemAwardEmojiQuery,
variables: {
fullPath: this.workItemFullpath,
iid: this.workItemIid,
@@ -234,7 +238,6 @@ export default {
<template>
<div v-if="!isLoading" class="gl-mt-3">
<awards-list
- data-testid="work-item-award-list"
:awards="awards"
:can-award-emoji="$options.isLoggedIn"
:current-user-id="currentUserId"
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 460b5d35187..d352d66196a 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
@@ -86,7 +86,7 @@ export default {
</script>
<template>
- <div class="gl-mb-3 gl-text-gray-700">
+ <div class="gl-mb-3 gl-text-gray-700 gl-mt-3">
<work-item-state-badge v-if="workItemState" :work-item-state="workItemState" />
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
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 b7f3ac93cdb..77c573b47e4 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -244,13 +244,7 @@ export default {
@keydown.ctrl.enter="updateWorkItem"
/>
<div class="gl-display-flex">
- <gl-alert
- v-if="hasConflicts"
- :dismissible="false"
- variant="danger"
- class="gl-w-full"
- data-testid="work-item-description-conflicts"
- >
+ <gl-alert v-if="hasConflicts" :dismissible="false" variant="danger" class="gl-w-full">
<p>
{{
s__(
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index 07e03eba1d1..124e05db431 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -114,7 +114,7 @@ export default {
v-else
ref="gfm-content"
v-safe-html="descriptionHtml"
- class="md gl-mb-5 gl-min-h-8"
+ class="md gl-mb-5 gl-min-h-8 gl-clearfix"
data-testid="work-item-description"
@change="toggleCheckboxes"
></div>
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 53929775684..45d3aa564a5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -50,7 +50,6 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemNotes from './work_item_notes.vue';
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';
@@ -61,7 +60,6 @@ export default {
},
isLoggedIn: isLoggedIn(),
components: {
- WorkItemStateToggleButton,
GlAlert,
GlButton,
GlLoadingIcon,
@@ -146,9 +144,9 @@ export default {
if (isEmpty(this.workItem)) {
this.setEmptyState();
}
- if (!this.isModal && this.workItem.project) {
- const path = this.workItem.project?.fullPath
- ? ` · ${this.workItem.project.fullPath}`
+ if (!this.isModal && this.workItem.namespace) {
+ const path = this.workItem.namespace.fullPath
+ ? ` · ${this.workItem.namespace.fullPath}`
: '';
document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`;
@@ -181,19 +179,19 @@ export default {
return this.workItemType ? `#${this.workItem.iid}` : '';
},
canUpdate() {
- return this.workItem?.userPermissions?.updateWorkItem;
+ return this.workItem.userPermissions?.updateWorkItem;
},
canDelete() {
- return this.workItem?.userPermissions?.deleteWorkItem;
+ return this.workItem.userPermissions?.deleteWorkItem;
},
canSetWorkItemMetadata() {
- return this.workItem?.userPermissions?.setWorkItemMetadata;
+ return this.workItem.userPermissions?.setWorkItemMetadata;
},
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
projectFullPath() {
- return this.workItem?.project?.fullPath;
+ return this.workItem.namespace?.fullPath;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
@@ -222,7 +220,7 @@ export default {
return this.parentWorkItem?.webUrl;
},
workItemIconName() {
- return this.workItem?.workItemType?.iconName;
+ return this.workItem.workItemType?.iconName;
},
noAccessSvgPath() {
return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
@@ -274,6 +272,18 @@ export default {
showWorkItemLinkedItems() {
return this.hasLinkedWorkItems && this.workItemLinkedItems;
},
+ titleClassHeader() {
+ return {
+ 'gl-sm-display-none!': this.parentWorkItem,
+ 'gl-w-full': !this.parentWorkItem,
+ };
+ },
+ titleClassComponent() {
+ return {
+ 'gl-sm-display-block!': !this.parentWorkItem,
+ 'gl-display-none gl-sm-display-block!': this.parentWorkItem,
+ };
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -285,7 +295,7 @@ export default {
},
methods: {
isWidgetPresent(type) {
- return this.workItem?.widgets?.find((widget) => widget.type === type);
+ return this.workItem.widgets?.find((widget) => widget.type === type);
},
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
@@ -409,7 +419,20 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
+ <div class="gl-sm-display-none! gl-display-flex">
+ <gl-button
+ v-if="isModal"
+ class="gl-ml-auto"
+ category="tertiary"
+ data-testid="work-item-close"
+ icon="close"
+ :aria-label="__('Close')"
+ @click="$emit('close')"
+ />
+ </div>
+ <div
+ class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
+ >
<ul
v-if="parentWorkItem"
class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
@@ -440,53 +463,55 @@ export default {
</li>
</ul>
<div
- v-else-if="!error && !workItemLoading"
- class="gl-mr-auto"
+ v-if="!error && !workItemLoading"
+ :class="titleClassHeader"
data-testid="work-item-type"
>
- <work-item-type-icon
- :work-item-icon-name="workItemIconName"
+ <work-item-title
+ v-if="workItem.title"
+ ref="title"
+ class="gl-sm-display-block!"
+ :work-item-id="workItem.id"
+ :work-item-title="workItem.title"
:work-item-type="workItemType"
- show-text
+ :work-item-parent-id="workItemParentId"
+ :can-update="canUpdate"
+ @error="updateError = $event"
+ />
+ </div>
+ <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3">
+ <work-item-todos
+ v-if="showWorkItemCurrentUserTodos"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItemIid"
+ :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"
+ :work-item-type-id="workItemTypeId"
+ :can-delete="canDelete"
+ :can-update="canUpdate"
+ :is-confidential="workItem.confidential"
+ :is-parent-confidential="parentWorkItemConfidentiality"
+ :work-item-reference="workItem.reference"
+ :work-item-create-note-email="workItem.createNoteEmail"
+ :is-modal="isModal"
+ :work-item-state="workItem.state"
+ :work-item-parent-id="workItemParentId"
+ @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
+ @toggleWorkItemConfidentiality="toggleConfidentiality"
+ @error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
/>
- {{ workItemBreadcrumbReference }}
</div>
- <work-item-state-toggle-button
- v-if="canUpdate"
- :work-item-id="workItem.id"
- :work-item-state="workItem.state"
- :work-item-parent-id="workItemParentId"
- :work-item-type="workItemType"
- @error="updateError = $event"
- />
- <work-item-todos
- v-if="showWorkItemCurrentUserTodos"
- :work-item-id="workItem.id"
- :work-item-iid="workItemIid"
- :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"
- :work-item-type-id="workItemTypeId"
- :can-delete="canDelete"
- :can-update="canUpdate"
- :is-confidential="workItem.confidential"
- :is-parent-confidential="parentWorkItemConfidentiality"
- :work-item-reference="workItem.reference"
- :work-item-create-note-email="workItem.createNoteEmail"
- :is-modal="isModal"
- @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
- @toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="updateError = $event"
- @promotedToObjective="$emit('promotedToObjective', workItemIid)"
- />
<gl-button
v-if="isModal"
+ class="gl-display-none gl-sm-display-block!"
category="tertiary"
data-testid="work-item-close"
icon="close"
@@ -496,8 +521,9 @@ export default {
</div>
<div>
<work-item-title
- v-if="workItem.title"
+ v-if="workItem.title && parentWorkItem"
ref="title"
+ :class="titleClassComponent"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 1aa62a2b906..704fe6fb11d 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlDatepicker, GlFormGroup } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { getDateWithUTC, newDateAsLocaleTime } from '~/lib/utils/datetime/date_calculation_utility';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
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 3cdbf816421..7a5d3b1155f 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -3,7 +3,8 @@ import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
-import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import groupLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -90,7 +91,9 @@ export default {
},
},
searchLabels: {
- query: labelSearchQuery,
+ query() {
+ return this.isGroup ? groupLabelsQuery : projectLabelsQuery;
+ },
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 f4de7c1dddc..b6ea09edbd4 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
@@ -1,7 +1,7 @@
<script>
-import * as Sentry from '@sentry/browser';
import produce from 'immer';
import Draggable from 'vuedraggable';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -50,6 +50,11 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -151,9 +156,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0];
},
- context: {
- isSingleRequest: true,
- },
});
},
prefetchWorkItem({ iid }) {
@@ -280,6 +282,7 @@ export default {
:confidential="child.confidential"
:work-item-type="workItemType"
:has-indirect-children="hasIndirectChildren"
+ :show-labels="showLabels"
@mouseover="prefetchWorkItem(child)"
@mouseout="clearPrefetching"
@removeChild="removeChild"
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 847a3585ac4..49454c3d9f3 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
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/alert';
@@ -49,6 +49,11 @@ export default {
required: false,
default: '',
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -231,6 +236,7 @@ export default {
:can-update="canUpdate"
:parent-work-item-id="issuableGid"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@click="$emit('click', $event)"
@removeChild="$emit('removeChild', childItem)"
/>
@@ -241,6 +247,7 @@ export default {
:work-item-id="issuableGid"
:work-item-type="workItemType"
:children="children"
+ :show-labels="showLabels"
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
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 7fa6ac2c57f..dd0a26c0b9c 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
@@ -5,6 +5,7 @@ import {
GlIcon,
GlLoadingIcon,
GlTooltipDirective,
+ GlToggle,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
@@ -15,7 +16,12 @@ import { isMetaKey } from '~/lib/utils/common_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import { FORM_TYPES, WIDGET_ICONS, WORK_ITEM_STATUS_TEXT } from '../../constants';
+import {
+ FORM_TYPES,
+ WIDGET_ICONS,
+ WORK_ITEM_STATUS_TEXT,
+ I18N_WORK_ITEM_SHOW_LABELS,
+} from '../../constants';
import { findHierarchyWidgetChildren } from '../../utils';
import { removeHierarchyChild } from '../../graphql/cache_utils';
import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
@@ -36,6 +42,7 @@ export default {
WorkItemDetailModal,
AbuseCategorySelector,
WorkItemChildrenWrapper,
+ GlToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -65,9 +72,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0] ?? {};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return !this.iid;
},
@@ -107,6 +111,7 @@ export default {
reportedUserId: 0,
reportedUrl: '',
widgetName: 'tasks',
+ showLabels: true,
};
},
computed: {
@@ -204,6 +209,7 @@ export default {
addChildButtonLabel: s__('WorkItem|Add'),
addChildOptionLabel: s__('WorkItem|Existing task'),
createChildOptionLabel: s__('WorkItem|New task'),
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
},
WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
WORK_ITEM_STATUS_TEXT,
@@ -227,6 +233,14 @@ export default {
</span>
</template>
<template #header-right>
+ <gl-toggle
+ class="gl-mr-4"
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<gl-disclosure-dropdown
v-if="canUpdate && canAddTask"
placement="right"
@@ -282,6 +296,7 @@ export default {
:full-path="fullPath"
:work-item-id="issuableGid"
:work-item-iid="iid"
+ :show-labels="showLabels"
@error="error = $event"
@show-modal="openChild"
/>
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 b61b3b2e0d3..3d09a90169c 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
@@ -1,10 +1,12 @@
<script>
+import { GlToggle } from '@gitlab/ui';
import {
FORM_TYPES,
WIDGET_TYPE_HIERARCHY,
WORK_ITEMS_TREE_TEXT_MAP,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ I18N_WORK_ITEM_SHOW_LABELS,
} from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import OkrActionsSplitButton from './okr_actions_split_button.vue';
@@ -21,6 +23,7 @@ export default {
WidgetWrapper,
WorkItemLinksForm,
WorkItemChildrenWrapper,
+ GlToggle,
},
props: {
fullPath: {
@@ -68,6 +71,7 @@ export default {
formType: null,
childType: null,
widgetName: 'tasks',
+ showLabels: true,
};
},
computed: {
@@ -99,6 +103,9 @@ export default {
this.$emit('show-modal', { event, modalWorkItem: child });
},
},
+ i18n: {
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
+ },
};
</script>
@@ -114,6 +121,14 @@ export default {
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
</template>
<template #header-right>
+ <gl-toggle
+ class="gl-mr-4"
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<okr-actions-split-button
v-if="canUpdate"
@showCreateObjectiveForm="
@@ -160,6 +175,7 @@ export default {
:work-item-id="workItemId"
:work-item-iid="workItemIid"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@error="error = $event"
@show-modal="showModal"
/>
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 401223c3593..af181fa4e7e 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
@@ -22,6 +22,11 @@ export default {
required: false,
default: false,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -35,6 +40,7 @@ export default {
:issuable-gid="workItemId"
:child-item="child"
:work-item-type="workItemType"
+ :show-labels="showLabels"
@removeChild="$emit('removeChild', $event)"
@click="$emit('click', Object.assign($event, { childItem: child }))"
/>
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 a2cbb7f7598..9c6fa158169 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -1,15 +1,7 @@
<script>
-import {
- GlFormGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlSkeletonLoader,
- GlSearchBoxByType,
- GlDropdownText,
-} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import { GlCollapsibleListbox, GlFormGroup, GlSkeletonLoader } from '@gitlab/ui';
import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
@@ -22,7 +14,8 @@ import {
TRACKING_CATEGORY_SHOW,
} from '../constants';
-const noMilestoneId = 'no-milestone-id';
+export const noMilestoneId = 'no-milestone-id';
+const noMilestoneItem = { text: s__('WorkItem|No milestone'), value: noMilestoneId };
export default {
i18n: {
@@ -37,13 +30,9 @@ export default {
EXPIRED_TEXT: __('(expired)'),
},
components: {
+ GlCollapsibleListbox,
GlFormGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
GlSkeletonLoader,
- GlSearchBoxByType,
- GlDropdownText,
},
mixins: [Tracking.mixin()],
props: {
@@ -74,11 +63,23 @@ export default {
data() {
return {
localMilestone: this.workItemMilestone,
+ localMilestoneId: this.workItemMilestone?.id,
searchTerm: '',
shouldFetch: false,
updateInProgress: false,
- isFocused: false,
milestones: [],
+ dropdownGroups: [
+ {
+ text: this.$options.i18n.NO_MILESTONE,
+ textSrOnly: true,
+ options: [noMilestoneItem],
+ },
+ {
+ text: __('Milestones'),
+ textSrOnly: true,
+ options: [],
+ },
+ ],
};
},
computed: {
@@ -103,23 +104,29 @@ export default {
isLoadingMilestones() {
return this.$apollo.queries.milestones.loading;
},
- isNoMilestone() {
- return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ milestonesList() {
+ return (
+ this.milestones.map(({ id, title, expired }) => {
+ return {
+ value: id,
+ text: title,
+ expired,
+ };
+ }) ?? []
+ );
},
- dropdownClasses() {
- return {
- 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
- 'is-not-focused': !this.isFocused,
- 'gl-min-w-20': true,
- };
+ toggleClasses() {
+ const toggleClasses = ['gl-max-w-full'];
+
+ if (this.localMilestoneId === noMilestoneId) {
+ toggleClasses.push('gl-text-gray-500!');
+ }
+ return toggleClasses;
},
},
watch: {
- workItemMilestone: {
- handler(newVal) {
- this.localMilestone = newVal;
- },
- deep: true,
+ milestones() {
+ this.dropdownGroups[1].options = this.milestonesList;
},
},
created() {
@@ -152,15 +159,11 @@ export default {
this.localMilestone = milestone;
},
onDropdownShown() {
- this.$refs.search.focusInput();
this.shouldFetch = true;
- this.isFocused = true;
},
onDropdownHide() {
- this.isFocused = false;
this.searchTerm = '';
this.shouldFetch = false;
- this.updateMilestone();
},
setSearchKey(value) {
this.searchTerm = value;
@@ -169,6 +172,9 @@ export default {
return this.localMilestone?.id === milestone?.id;
},
updateMilestone() {
+ this.localMilestone =
+ this.milestones.find(({ id }) => id === this.localMilestoneId) ?? noMilestoneItem;
+
if (this.workItemMilestone?.id === this.localMilestone?.id) {
return;
}
@@ -182,8 +188,7 @@ export default {
input: {
id: this.workItemId,
milestoneWidget: {
- milestoneId:
- this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id,
+ milestoneId: this.localMilestoneId === noMilestoneId ? null : this.localMilestoneId,
},
},
},
@@ -222,50 +227,45 @@ export default {
>
{{ dropdownText }}
</span>
- <gl-dropdown
+
+ <gl-collapsible-listbox
v-else
id="milestone-value"
+ v-model="localMilestoneId"
+ :items="dropdownGroups"
+ category="tertiary"
data-testid="work-item-milestone-dropdown"
- class="gl-pl-0 gl-max-w-full work-item-field-value"
- :toggle-class="dropdownClasses"
- :text="dropdownText"
+ class="gl-max-w-full"
+ :toggle-text="dropdownText"
:loading="updateInProgress"
+ :toggle-class="toggleClasses"
+ searchable
+ @select="updateMilestone"
@shown="onDropdownShown"
- @hide="onDropdownHide"
+ @hidden="onDropdownHide"
+ @search="debouncedSearchKeyUpdate"
>
- <template #header>
- <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ <template #list-item="{ item }">
+ {{ item.text }}
+ <span v-if="item.expired">{{ $options.i18n.EXPIRED_TEXT }}</span>
</template>
- <gl-dropdown-item
- data-testid="no-milestone"
- is-check-item
- :is-checked="isNoMilestone"
- @click="handleMilestoneClick({ id: 'no-milestone-id' })"
- >
- {{ $options.i18n.NO_MILESTONE }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-text v-if="isLoadingMilestones">
- <gl-skeleton-loader :height="90">
+ <template #footer>
+ <gl-skeleton-loader v-if="isLoadingMilestones" :height="90">
<rect width="380" height="10" x="10" y="15" rx="4" />
<rect width="280" height="10" x="10" y="30" rx="4" />
<rect width="380" height="10" x="10" y="50" rx="4" />
<rect width="280" height="10" x="10" y="65" rx="4" />
</gl-skeleton-loader>
- </gl-dropdown-text>
- <template v-else-if="milestones.length">
- <gl-dropdown-item
- v-for="milestone in milestones"
- :key="milestone.id"
- is-check-item
- :is-checked="isMilestoneChecked(milestone)"
- @click="handleMilestoneClick(milestone)"
+
+ <div
+ v-else-if="!milestones.length"
+ aria-live="assertive"
+ class="gl-pl-7 gl-pr-5 gl-py-3 gl-font-base gl-text-gray-600"
+ data-testid="no-results-text"
>
- {{ milestone.title }}
- <template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template>
- </gl-dropdown-item>
+ {{ $options.i18n.NO_MATCHING_RESULTS }}
+ </div>
</template>
- <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</gl-form-group>
</template>
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 fe8aea99f53..6756acd4495 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
import { TYPENAME_DISCUSSION, TYPENAME_NOTE } from '~/graphql_shared/constants';
@@ -170,9 +170,6 @@ export default {
apollo: {
workItemNotes: {
query: workItemNotesByIidQuery,
- context: {
- isSingleRequest: true,
- },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue
index e16299f482f..ce30f7985cf 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent.vue
@@ -1,18 +1,20 @@
<script>
import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
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 { removeHierarchyChild } from '../graphql/cache_utils';
+import groupWorkItemsQuery from '../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
- WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ SUPPORTED_PARENT_TYPE_MAP,
} from '../constants';
export default {
@@ -31,7 +33,7 @@ export default {
GlCollapsibleListbox,
},
mixins: [glFeatureFlagMixin()],
- inject: ['fullPath'],
+ inject: ['fullPath', 'isGroup'],
props: {
workItemId: {
type: String,
@@ -60,7 +62,7 @@ export default {
searchStarted: false,
availableWorkItems: [],
localSelectedItem: this.parent?.id,
- isNotFocused: true,
+ oldParent: this.parent,
};
},
computed: {
@@ -80,13 +82,8 @@ export default {
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,
- };
+ parentType() {
+ return SUPPORTED_PARENT_TYPE_MAP[this.workItemType];
},
},
watch: {
@@ -101,13 +98,17 @@ export default {
},
apollo: {
availableWorkItems: {
- query: projectWorkItemsQuery,
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
searchTerm: this.search,
- types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ types: this.parentType,
in: this.search ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
};
},
skip() {
@@ -146,6 +147,14 @@ export default {
},
},
},
+ update: (cache) =>
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.oldParent?.iid,
+ isGroup: this.isGroup,
+ workItem: { id: this.workItemId },
+ }),
});
if (errors.length) {
@@ -171,19 +180,10 @@ export default {
},
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;
- }
},
},
};
@@ -206,30 +206,20 @@ export default {
>
{{ listboxText }}
</span>
- <div
- v-else
- :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"
- @mouseover="isNotFocused = false"
- @mouseleave="setListboxFocused"
- @focusout="isNotFocused = true"
- @focusin="isNotFocused = false"
- >
+ <div v-else :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }">
<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"
+ category="tertiary"
: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"
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
index d242db95896..c98bd6ce1e9 100644
--- 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
@@ -4,6 +4,7 @@ import { GlFormGroup, GlForm, GlFormRadioGroup, GlButton, GlAlert } from '@gitla
import { __, s__ } from '~/locale';
import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import addLinkedItemsMutation from '../../graphql/add_linked_items.mutation.graphql';
+import groupWorkItemByIidQuery from '../../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import {
LINK_ITEM_FORM_HEADER_LABEL,
@@ -23,6 +24,7 @@ export default {
GlAlert,
WorkItemTokenInput,
},
+ inject: ['isGroup'],
props: {
workItemId: {
type: String,
@@ -121,7 +123,7 @@ export default {
},
) => {
const queryArgs = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
};
const sourceData = cache.readQuery(queryArgs);
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 002c1786044..e70c79ea68f 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
@@ -19,6 +19,11 @@ export default {
type: Boolean,
required: true,
},
+ showLabels: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
};
</script>
@@ -42,6 +47,7 @@ export default {
:child-item="linkedItem.workItem"
:can-update="canUpdate"
:show-task-icon="true"
+ :show-labels="showLabels"
@click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
@removeChild="$emit('removeLinkedItem', linkedItem.workItem)"
/>
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 20427fe96c4..790804a8934 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,6 +1,6 @@
<script>
import { produce } from 'immer';
-import { GlLoadingIcon, GlIcon, GlButton, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlButton, GlLink, GlToggle } from '@gitlab/ui';
import { s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -8,7 +8,11 @@ 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 {
+ WIDGET_TYPE_LINKED_ITEMS,
+ LINKED_CATEGORIES_MAP,
+ I18N_WORK_ITEM_SHOW_LABELS,
+} from '../../constants';
import WidgetWrapper from '../widget_wrapper.vue';
import WorkItemRelationshipList from './work_item_relationship_list.vue';
@@ -24,6 +28,7 @@ export default {
WidgetWrapper,
WorkItemRelationshipList,
WorkItemAddRelationshipForm,
+ GlToggle,
},
inject: ['isGroup'],
props: {
@@ -60,9 +65,6 @@ export default {
update(data) {
return data.workspace.workItems.nodes[0] ?? {};
},
- context: {
- isSingleRequest: true,
- },
skip() {
return !this.workItemIid;
},
@@ -97,6 +99,7 @@ export default {
linksBlocks: [],
isShownLinkItemForm: false,
widgetName: 'linkeditems',
+ showLabels: true,
};
},
computed: {
@@ -150,7 +153,7 @@ export default {
return;
}
const queryArgs = {
- query: workItemByIidQuery,
+ query: this.isGroup ? groupWorkItemByIidQuery : workItemByIidQuery,
variables: { fullPath: this.workItemFullPath, iid: this.workItemIid },
};
const sourceData = cache.readQuery(queryArgs);
@@ -200,6 +203,7 @@ export default {
blockingTitle: s__('WorkItem|Blocking'),
blockedByTitle: s__('WorkItem|Blocked by'),
addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
+ showLabelsLabel: I18N_WORK_ITEM_SHOW_LABELS,
},
};
</script>
@@ -222,11 +226,18 @@ export default {
</div>
</template>
<template #header-right>
+ <gl-toggle
+ :value="showLabels"
+ :label="$options.i18n.showLabelsLabel"
+ label-position="left"
+ label-id="relationship-toggle-labels"
+ @change="showLabels = $event"
+ />
<gl-button
v-if="canAdminWorkItemLink"
data-testid="link-item-add-button"
size="small"
- class="gl-ml-3"
+ class="gl-ml-4"
@click="showLinkItemForm"
>
<slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
@@ -264,6 +275,7 @@ export default {
:linked-items="linksBlocks"
:heading="$options.i18n.blockingTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
@@ -276,6 +288,7 @@ export default {
:linked-items="linksIsBlockedBy"
:heading="$options.i18n.blockedByTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
@@ -284,6 +297,7 @@ export default {
:linked-items="linksRelatesTo"
:heading="$options.i18n.relatedToTitle"
:can-update="canAdminWorkItemLink"
+ :show-labels="showLabels"
@showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
@removeLinkedItem="removeLinkedItem"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
index 0ea30845466..581ef9ec945 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_toggle_button.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
@@ -1,9 +1,8 @@
<script>
-import { GlButton } from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
+import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
-import { __, sprintf } from '~/locale';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { __ } from '~/locale';
import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
@@ -17,6 +16,8 @@ import {
export default {
components: {
GlButton,
+ GlDisclosureDropdownItem,
+ GlLoadingIcon,
},
mixins: [Tracking.mixin()],
props: {
@@ -37,6 +38,11 @@ export default {
required: false,
default: null,
},
+ showAsDropdownItem: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -51,9 +57,7 @@ export default {
const baseText = this.isWorkItemOpen
? __('Close %{workItemType}')
: __('Reopen %{workItemType}');
- return capitalizeFirstCharacter(
- sprintf(baseText, { workItemType: this.workItemType.toLowerCase() }),
- );
+ return sprintfWorkItem(baseText, this.workItemType);
},
tracking() {
return {
@@ -62,6 +66,12 @@ export default {
property: `type_${this.workItemType}`,
};
},
+ toggleInProgressText() {
+ const baseText = this.isWorkItemOpen
+ ? __('Closing %{workItemType}')
+ : __('Reopening %{workItemType}');
+ return sprintfWorkItem(baseText, this.workItemType);
+ },
},
methods: {
async updateWorkItem() {
@@ -104,10 +114,18 @@ export default {
</script>
<template>
- <gl-button
- :loading="updateInProgress"
- data-testid="work-item-state-toggle"
- @click="updateWorkItem"
- >{{ toggleWorkItemStateText }}</gl-button
- >
+ <gl-disclosure-dropdown-item v-if="showAsDropdownItem" @action="updateWorkItem">
+ <template #list-item>
+ <template v-if="updateInProgress">
+ <gl-loading-icon inline size="sm" />
+ {{ toggleInProgressText }}
+ </template>
+ <template v-else>
+ {{ toggleWorkItemStateText }}
+ </template>
+ </template>
+ </gl-disclosure-dropdown-item>
+ <gl-button v-else :loading="updateInProgress" @click="updateWorkItem">{{
+ toggleWorkItemStateText
+ }}</gl-button>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index c52a6854fad..9b5803421dd 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -1,10 +1,12 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
TRACKING_CATEGORY_SHOW,
+ WORK_ITEM_TITLE_MAX_LENGTH,
+ I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE,
} from '../constants';
import { getUpdateWorkItemMutation } from './update_work_item';
import ItemTitle from './item_title.vue';
@@ -56,6 +58,11 @@ export default {
return;
}
+ if (updatedTitle.length > WORK_ITEM_TITLE_MAX_LENGTH) {
+ this.$emit('error', sprintfWorkItem(I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE));
+ return;
+ }
+
const input = {
id: this.workItemId,
title: updatedTitle,
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 e6d7f2067ba..62518616398 100644
--- a/app/assets/javascripts/work_items/components/work_item_todos.vue
+++ b/app/assets/javascripts/work_items/components/work_item_todos.vue
@@ -175,17 +175,12 @@ export default {
<template>
<gl-button
v-gl-tooltip.hover
- data-testid="work-item-todos-action"
:loading="isLoading"
:title="buttonLabel"
- category="tertiary"
+ category="secondary"
:aria-label="buttonLabel"
@click="onToggle"
>
- <gl-icon
- data-testid="work-item-todos-icon"
- :class="{ 'gl-fill-blue-500': pendingTodo }"
- :name="buttonIcon"
- />
+ <gl-icon :class="{ 'gl-fill-blue-500': pendingTodo }" :name="buttonIcon" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index a64172acff4..daa72204609 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -35,6 +35,7 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_ENUM_EPIC = 'EPIC';
export const WORK_ITEM_TYPE_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
@@ -45,6 +46,8 @@ export const WORK_ITEM_TYPE_VALUE_REQUIREMENTS = 'Requirements';
export const WORK_ITEM_TYPE_VALUE_KEY_RESULT = 'Key Result';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
+export const WORK_ITEM_TITLE_MAX_LENGTH = 255;
+
export const i18n = {
fetchErrorTitle: s__('WorkItem|Work item not found'),
fetchError: s__(
@@ -91,8 +94,9 @@ export const I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR = s__(
export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
-export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
- 'WorkItem|Search existing %{workItemType}s',
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__('WorkItem|Search existing items');
+export const I18N_WORK_ITEM_SEARCH_ERROR = s__(
+ 'WorkItem|Something went wrong while fetching the %{workItemType}. Please try again.',
);
export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__(
'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access',
@@ -108,6 +112,11 @@ export const I18N_WORK_ITEM_ERROR_COPY_EMAIL = s__(
'WorkItem|Something went wrong while copying the %{workItemType} email address. Please try again.',
);
+export const I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE = sprintf(
+ s__('WorkItem|Title cannot have more than %{WORK_ITEM_TITLE_MAX_LENGTH} characters.'),
+ { WORK_ITEM_TITLE_MAX_LENGTH },
+);
+
export const I18N_WORK_ITEM_COPY_CREATE_NOTE_EMAIL = s__(
'WorkItem|Copy %{workItemType} email address',
);
@@ -122,6 +131,7 @@ 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 I18N_WORK_ITEM_SHOW_LABELS = s__('WorkItem|Show labels');
export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
const workItemType = workItemTypeArg || s__('WorkItem|item');
@@ -178,6 +188,11 @@ export const WORK_ITEMS_TYPE_MAP = {
name: s__('WorkItem|Key result'),
value: WORK_ITEM_TYPE_VALUE_KEY_RESULT,
},
+ [WORK_ITEM_TYPE_ENUM_EPIC]: {
+ icon: `epic`,
+ name: s__('WorkItem|Epic'),
+ value: WORK_ITEM_TYPE_VALUE_EPIC,
+ },
};
export const WORK_ITEMS_TREE_TEXT_MAP = {
@@ -246,12 +261,12 @@ export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
];
export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
-export const TEST_ID_NOTIFICATIONS_TOGGLE_ACTION = 'notifications-toggle-action';
export const TEST_ID_NOTIFICATIONS_TOGGLE_FORM = 'notifications-toggle-form';
export const TEST_ID_DELETE_ACTION = 'delete-action';
export const TEST_ID_PROMOTE_ACTION = 'promote-action';
export const TEST_ID_COPY_REFERENCE_ACTION = 'copy-reference-action';
export const TEST_ID_COPY_CREATE_NOTE_EMAIL_ACTION = 'copy-create-note-email-action';
+export const TEST_ID_TOGGLE_ACTION = 'state-toggle-action';
export const TODO_ADD_ICON = 'todo-add';
export const TODO_DONE_ICON = 'todo-done';
@@ -288,3 +303,9 @@ export const LINK_ITEM_FORM_HEADER_LABEL = {
[WORK_ITEM_TYPE_VALUE_KEY_RESULT]: s__('WorkItem|The current key result'),
[WORK_ITEM_TYPE_VALUE_TASK]: s__('WorkItem|The current task'),
};
+
+export const SUPPORTED_PARENT_TYPE_MAP = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ [WORK_ITEM_TYPE_VALUE_TASK]: [WORK_ITEM_TYPE_ENUM_ISSUE],
+};
diff --git a/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql b/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
index 82a532e1bea..0b9dc546df3 100644
--- a/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/award_emoji.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
#import "~/work_items/graphql/award_emoji.fragment.graphql"
-query workItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+query projectWorkItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
workspace: project(fullPath: $fullPath) {
id
workItems(iid: $iid) {
diff --git a/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql b/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql
new file mode 100644
index 00000000000..cdf8c7cad04
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_award_emoji.query.graphql
@@ -0,0 +1,27 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/award_emoji.fragment.graphql"
+
+query groupWorkItemAwardEmojis($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetAwardEmoji {
+ type
+ awardEmoji(first: $pageSize, after: $after) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...AwardEmojiFragment
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
new file mode 100644
index 00000000000..5332e21a0cb
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_items.query.graphql
@@ -0,0 +1,17 @@
+query groupWorkItems(
+ $searchTerm: String
+ $fullPath: ID!
+ $types: [IssueType!]
+ $in: [IssuableSearchableField!]
+) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(search: $searchTerm, types: $types, in: $in) {
+ nodes {
+ id
+ iid
+ title
+ }
+ }
+ }
+}
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 2be436aa8c2..3aeaaa1116a 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
@@ -3,6 +3,8 @@ query projectWorkItems(
$fullPath: ID!
$types: [IssueType!]
$in: [IssuableSearchableField!]
+ $iid: String = null
+ $isNumber: Boolean!
) {
workspace: project(fullPath: $fullPath) {
id
@@ -11,8 +13,13 @@ query projectWorkItems(
id
iid
title
- state
- confidential
+ }
+ }
+ workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) {
+ nodes {
+ id
+ iid
+ title
}
}
}
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 fac99310890..ef43b9c026d 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -4,6 +4,7 @@
fragment WorkItem on WorkItem {
id
iid
+ archived
title
state
description
@@ -13,10 +14,9 @@ fragment WorkItem on WorkItem {
closedAt
reference(full: true)
createNoteEmail
- project {
+ namespace {
id
fullPath
- archived
name
}
author {
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index a853018a931..58f74dccd4d 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { STATUS_OPEN } from '~/issues/constants';
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 31e790254d9..435a1233dce 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -103,7 +103,7 @@ export default {
data: {
workspace: {
__typename: TYPENAME_PROJECT,
- id: workItem.project.id,
+ id: workItem.namespace.id,
workItems: {
__typename: 'WorkItemConnection',
nodes: [workItem],