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')
-rw-r--r--app/assets/images/auth_buttons/salesforce_64.pngbin2012 -> 0 bytes
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue17
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue3
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_app.vue46
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue19
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue42
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql13
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql11
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql10
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/history_items.vue51
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/labels_select.vue235
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_actions.vue8
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_details.vue49
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_header.vue16
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue13
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/user_details.vue40
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js4
-rw-r--r--app/assets/javascripts/admin/abuse_report/index.js18
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue21
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue3
-rw-r--r--app/assets/javascripts/admin/users/components/actions/approve.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue3
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_form.vue8
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue11
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue3
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue3
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/api/application_settings_api.js14
-rw-r--r--app/assets/javascripts/authentication/webauthn/error.js9
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js1
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue26
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js8
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js16
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue61
-rw-r--r--app/assets/javascripts/blob/line_highlighter.js19
-rw-r--r--app/assets/javascripts/blob/openapi/index.js9
-rw-r--r--app/assets/javascripts/blob/queries/application_info.query.graphql (renamed from app/assets/javascripts/repository/queries/application_info.query.graphql)0
-rw-r--r--app/assets/javascripts/blob/queries/user_info.query.graphql (renamed from app/assets/javascripts/repository/queries/user_info.query.graphql)0
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue9
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue10
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue5
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue9
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue5
-rw-r--r--app/assets/javascripts/boards/graphql/cache_updates.js4
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_members.query.graphql15
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_members.query.graphql15
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js40
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue)80
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/cancel_jobs.vue)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/cancel_jobs_modal.vue)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue)0
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue (renamed from app/assets/javascripts/pages/admin/jobs/components/jobs_skeleton_loader.vue)0
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/constants.js (renamed from app/assets/javascripts/pages/admin/jobs/components/constants.js)2
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js (renamed from app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js)0
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql (renamed from app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql)9
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql5
-rw-r--r--app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql (renamed from app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/artifacts/components/feedback_banner.vue41
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue3
-rw-r--r--app/assets/javascripts/ci/artifacts/constants.js7
-rw-r--r--app/assets/javascripts/ci/artifacts/index.js8
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js262
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue195
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue5
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue3
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue49
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js25
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/utils.js10
-rw-r--r--app/assets/javascripts/ci/common/pipelines_table.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue)26
-rw-r--r--app/assets/javascripts/ci/common/private/job_action_component.vue (renamed from app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue)2
-rw-r--r--app/assets/javascripts/ci/common/private/job_links_layer.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue)6
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue (renamed from app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue)0
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue99
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js23
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue79
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue (renamed from app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue)0
-rw-r--r--app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js22
-rw-r--r--app/assets/javascripts/ci/constants.js51
-rw-r--r--app/assets/javascripts/ci/event_hub.js (renamed from app/assets/javascripts/jobs/components/table/event_hub.js)0
-rw-r--r--app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/empty_state.vue (renamed from app/assets/javascripts/jobs/components/job/empty_state.vue)2
-rw-r--r--app/assets/javascripts/ci/job_details/components/environments_block.vue (renamed from app/assets/javascripts/jobs/components/job/environments_block.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/erased_block.vue (renamed from app/assets/javascripts/jobs/components/job/erased_block.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue (renamed from app/assets/javascripts/vue_shared/components/header_ci_component.vue)48
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue (renamed from app/assets/javascripts/jobs/components/job/job_log_controllers.vue)17
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue (renamed from app/assets/javascripts/jobs/components/log/collapsible_section.vue)16
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/duration_badge.vue (renamed from app/assets/javascripts/jobs/components/log/duration_badge.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue (renamed from app/assets/javascripts/jobs/components/log/line.vue)26
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue (renamed from app/assets/javascripts/jobs/components/log/line_header.vue)28
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue (renamed from app/assets/javascripts/jobs/components/log/line_number.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/log.vue (renamed from app/assets/javascripts/jobs/components/log/log.vue)10
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/utils.js12
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue (renamed from app/assets/javascripts/jobs/components/job/manual_variables_form.vue)12
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue)7
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue54
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue34
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue)10
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue)12
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_sidebar_retry_button.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue)7
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/jobs_container.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue)3
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue)75
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue)29
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue)56
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue)12
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue)44
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/trigger_block.vue (renamed from app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue)2
-rw-r--r--app/assets/javascripts/ci/job_details/components/stuck_block.vue (renamed from app/assets/javascripts/jobs/components/job/stuck_block.vue)3
-rw-r--r--app/assets/javascripts/ci/job_details/components/unmet_prerequisites_block.vue (renamed from app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue)0
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/graphql/queries/get_job.query.graphql (renamed from app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js (renamed from app/assets/javascripts/jobs/index.js)2
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue (renamed from app/assets/javascripts/jobs/components/job/job_app.vue)33
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js (renamed from app/assets/javascripts/jobs/store/actions.js)2
-rw-r--r--app/assets/javascripts/ci/job_details/store/getters.js (renamed from app/assets/javascripts/jobs/store/getters.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/index.js (renamed from app/assets/javascripts/jobs/store/index.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js (renamed from app/assets/javascripts/jobs/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js (renamed from app/assets/javascripts/jobs/store/mutations.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/state.js (renamed from app/assets/javascripts/jobs/store/state.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js (renamed from app/assets/javascripts/jobs/store/utils.js)0
-rw-r--r--app/assets/javascripts/ci/job_details/utils.js29
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/actions_cell.vue)14
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/duration_cell.vue)9
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/job_cell.vue)6
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue (renamed from app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue)10
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table.vue)16
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue)1
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue)4
-rw-r--r--app/assets/javascripts/ci/jobs_page/constants.js (renamed from app/assets/javascripts/jobs/components/table/constants.js)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/event_hub.js (renamed from app/assets/javascripts/pipelines/event_hub.js)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/cache_config.js (renamed from app/assets/javascripts/jobs/components/table/graphql/cache_config.js)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/fragments/job.fragment.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_play.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql (renamed from app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/jobs_page/index.js (renamed from app/assets/javascripts/jobs/components/table/index.js)2
-rw-r--r--app/assets/javascripts/ci/jobs_page/jobs_page_app.vue (renamed from app/assets/javascripts/jobs/components/table/jobs_table_app.vue)12
-rw-r--r--app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue60
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql16
-rw-r--r--app/assets/javascripts/ci/mixins/delayed_job_mixin.js (renamed from app/assets/javascripts/jobs/mixins/delayed_job_mixin.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js (renamed from app/assets/javascripts/pipelines/constants.js)45
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue (renamed from app/assets/javascripts/pipelines/components/dag/dag_annotations.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue (renamed from app/assets/javascripts/pipelines/components/dag/dag_graph.vue)8
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/constants.js (renamed from app/assets/javascripts/pipelines/components/dag/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/dag.vue (renamed from app/assets/javascripts/pipelines/components/dag/dag.vue)15
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/dag/drawing_utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js (renamed from app/assets/javascripts/pipelines/components/dag/interactions.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/api_utils.js (renamed from app/assets/javascripts/pipelines/components/graph_shared/api.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue (renamed from app/assets/javascripts/pipelines/components/graph/graph_component.vue)12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue (renamed from app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue (renamed from app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue (renamed from app/assets/javascripts/pipelines/components/graph/job_item.vue)10
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue (renamed from app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue)14
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue (renamed from app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue)8
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/links_inner.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue)7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue (renamed from app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue (renamed from app/assets/javascripts/pipelines/components/graph/stage_column_component.vue)12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/constants.js (renamed from app/assets/javascripts/pipelines/components/graph/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue (renamed from app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue)12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/graphql/mutations/dismiss_pipeline_notification.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/perf_utils.js (renamed from app/assets/javascripts/pipelines/components/graph/perf_utils.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/utils.js (renamed from app/assets/javascripts/pipelines/components/graph/utils.js)7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql (renamed from app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/delete_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/retry_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/provider.js (renamed from app/assets/javascripts/pipelines/graphql/provider.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_details_header.vue)18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue (renamed from app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue)5
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/failed_jobs_app.vue (renamed from app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql (renamed from app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/jobs_app.vue (renamed from app/assets/javascripts/pipelines/components/jobs/jobs_app.vue)8
-rw-r--r--app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js (renamed from app/assets/javascripts/pipelines/mixins/pipelines_mixin.js)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_bundle.js (renamed from app/assets/javascripts/pipelines/pipeline_details_bundle.js)10
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js (renamed from app/assets/javascripts/pipelines/pipeline_details_header.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js (renamed from app/assets/javascripts/pipelines/pipeline_shared_client.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js (renamed from app/assets/javascripts/pipelines/pipeline_tabs.js)5
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js (renamed from app/assets/javascripts/pipelines/pipelines_index.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/routes.js (renamed from app/assets/javascripts/pipelines/routes.js)10
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/pipelines_store.js (renamed from app/assets/javascripts/pipelines/stores/pipelines_store.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/actions.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/actions.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/constants.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/getters.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/getters.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/index.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/index.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutation_types.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutations.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/mutations.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/state.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/state.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js (renamed from app/assets/javascripts/pipelines/stores/test_reports/utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/tabs/pipeline_tabs.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_tabs.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/empty_state.vue)1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue)7
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_reports.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_suite_table.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_summary.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_summary.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_summary_table.vue (renamed from app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js (renamed from app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/index.js (renamed from app/assets/javascripts/pipelines/utils.js)27
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js (renamed from app/assets/javascripts/pipelines/components/parsing_utils.js)4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/utils/unwrapping_utils.js (renamed from app/assets/javascripts/pipelines/components/unwrapping_utils.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue7
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/graph/job_pill.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/graph/pipeline_graph.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue)11
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/graph/stage_name.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue50
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue28
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/accessors/linked_pipelines_accessors.js (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stage.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/job_item.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue)10
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/pipeline_mini_graph.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue)13
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stage.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue)9
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stages.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue38
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue7
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue50
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ci_templates.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue)7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/utils.js (renamed from app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue)4
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_multi_actions.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue)107
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue)7
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_filtered_search.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue)10
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue)3
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/time_ago.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_actions.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql (renamed from app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs_count.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/pipelines.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue)33
-rw-r--r--app/assets/javascripts/ci/pipelines_page/services/pipelines_service.js (renamed from app/assets/javascripts/pipelines/services/pipelines_service.js)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/constants.js (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_branch_name_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue)5
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_source_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue)2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_status_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue)0
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_tag_name_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue)5
-rw-r--r--app/assets/javascripts/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue (renamed from app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue)9
-rw-r--r--app/assets/javascripts/ci/reports/components/issue_status_icon.vue5
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue3
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue28
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue7
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue16
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_create_form.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_form_fields.vue15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_managers_table.vue12
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_tabs.vue7
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_count.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_stats.vue1
-rw-r--r--app/assets/javascripts/ci/runner/constants.js3
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue34
-rw-r--r--app/assets/javascripts/ci/runner/project_runners/index.js23
-rw-r--r--app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue19
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js3
-rw-r--r--app/assets/javascripts/ci/utils.js (renamed from app/assets/javascripts/jobs/utils.js)13
-rw-r--r--app/assets/javascripts/ci_secure_files/components/metadata/modal.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue3
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue12
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue3
-rw-r--r--app/assets/javascripts/commit/constants.js6
-rw-r--r--app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue (renamed from app/assets/javascripts/commit/pipelines/pipelines_table.vue)12
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue0
-rw-r--r--app/assets/javascripts/commons/gitlab_ui.js10
-rw-r--r--app/assets/javascripts/commons/index.js1
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue2
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue28
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue3
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue25
-rw-r--r--app/assets/javascripts/contribution_events/components/contribution_events.vue10
-rw-r--r--app/assets/javascripts/contribution_events/components/target_link.vue2
-rw-r--r--app/assets/javascripts/contribution_events/constants.js26
-rw-r--r--app/assets/javascripts/create_item_dropdown.js4
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue10
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js12
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js2
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js3
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/index.js5
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js3
-rw-r--r--app/assets/javascripts/deprecated_notes.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_inline_findings_item.vue32
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue14
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue3
-rw-r--r--app/assets/javascripts/diffs/store/actions.js3
-rw-r--r--app/assets/javascripts/diffs/store/getters.js3
-rw-r--r--app/assets/javascripts/drawio/drawio_editor.js3
-rw-r--r--app/assets/javascripts/editor/schema/ci.json7
-rw-r--r--app/assets/javascripts/entrypoints/analytics.js17
-rw-r--r--app/assets/javascripts/entrypoints/jira_connect_app.js1
-rw-r--r--app/assets/javascripts/entrypoints/main.js6
-rw-r--r--app/assets/javascripts/entrypoints/main_ee.js5
-rw-r--r--app/assets/javascripts/entrypoints/main_jh.js5
-rw-r--r--app/assets/javascripts/entrypoints/performance_bar.js1
-rw-r--r--app/assets/javascripts/entrypoints/redirect_listbox.js1
-rw-r--r--app/assets/javascripts/entrypoints/sandboxed_mermaid.js1
-rw-r--r--app/assets/javascripts/entrypoints/sentry.js1
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue2
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue7
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue7
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue17
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue15
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue8
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment.query.graphql1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql21
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql16
-rw-r--r--app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js2
-rw-r--r--app/assets/javascripts/environments/mount_show.js3
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue147
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js2
-rw-r--r--app/assets/javascripts/frequent_items/utils.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js26
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue6
-rw-r--r--app/assets/javascripts/google_cloud/deployments/service_table.vue2
-rw-r--r--app/assets/javascripts/google_cloud/service_accounts/list.vue1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json5
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql12
-rw-r--r--app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql19
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue24
-rw-r--r--app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue17
-rw-r--r--app/assets/javascripts/groups/index.js13
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js38
-rw-r--r--app/assets/javascripts/ide/commit_icon.js3
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue103
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue6
-rw-r--r--app/assets/javascripts/ide/components/terminal/terminal.vue3
-rw-r--r--app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue6
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js3
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js6
-rw-r--r--app/assets/javascripts/ide/lib/errors.js6
-rw-r--r--app/assets/javascripts/ide/lib/files.js3
-rw-r--r--app/assets/javascripts/ide/lib/mirror.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js3
-rw-r--r--app/assets/javascripts/ide/stores/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js3
-rw-r--r--app/assets/javascripts/ide/stores/utils.js9
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue6
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue10
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue2
-rw-r--r--app/assets/javascripts/integrations/index/components/integrations_table.vue66
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue126
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue2
-rw-r--r--app/assets/javascripts/invite_members/constants.js20
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js3
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js6
-rw-r--r--app/assets/javascripts/issuable/components/csv_export_modal.vue4
-rw-r--r--app/assets/javascripts/issuable/components/csv_import_export_buttons.vue2
-rw-r--r--app/assets/javascripts/issuable/components/issue_assignees.vue4
-rw-r--r--app/assets/javascripts/issuable/components/issue_milestone.vue3
-rw-r--r--app/assets/javascripts/issuable/components/status_badge.vue98
-rw-r--r--app/assets/javascripts/issuable/components/status_box.vue146
-rw-r--r--app/assets/javascripts/issuable/index.js19
-rw-r--r--app/assets/javascripts/issuable/issuable_context.js23
-rw-r--r--app/assets/javascripts/issuable/popover/components/issue_popover.vue13
-rw-r--r--app/assets/javascripts/issues/constants.js1
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue1
-rw-r--r--app/assets/javascripts/issues/index.js38
-rw-r--r--app/assets/javascripts/issues/issue.js5
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue2
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue40
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue49
-rw-r--r--app/assets/javascripts/issues/list/constants.js1
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues/list/queries/search_milestones.query.graphql11
-rw-r--r--app/assets/javascripts/issues/list/queries/search_users.query.graphql29
-rw-r--r--app/assets/javascripts/issues/list/queries/user.fragment.graphql6
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue9
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/index.js10
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue (renamed from app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue)3
-rw-r--r--app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue (renamed from app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue)0
-rw-r--r--app/assets/javascripts/issues/service_desk/components/info_banner.vue (renamed from app/assets/javascripts/service_desk/components/info_banner.vue)0
-rw-r--r--app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue (renamed from app/assets/javascripts/service_desk/components/service_desk_list_app.vue)188
-rw-r--r--app/assets/javascripts/issues/service_desk/constants.js (renamed from app/assets/javascripts/service_desk/constants.js)5
-rw-r--r--app/assets/javascripts/issues/service_desk/graphql.js (renamed from app/assets/javascripts/service_desk/graphql.js)0
-rw-r--r--app/assets/javascripts/issues/service_desk/index.js (renamed from app/assets/javascripts/service_desk/index.js)6
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues.query.graphql (renamed from app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql)7
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql (renamed from app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql)11
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/issue.fragment.graphql (renamed from app/assets/javascripts/service_desk/queries/issue.fragment.graphql)1
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/label.fragment.graphql (renamed from app/assets/javascripts/service_desk/queries/label.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/milestone.fragment.graphql (renamed from app/assets/javascripts/service_desk/queries/milestone.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql13
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/search_project_labels.query.graphql (renamed from app/assets/javascripts/service_desk/queries/search_project_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/search_project_milestones.query.graphql (renamed from app/assets/javascripts/service_desk/queries/search_project_milestones.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql5
-rw-r--r--app/assets/javascripts/issues/service_desk/search_tokens.js (renamed from app/assets/javascripts/service_desk/search_tokens.js)0
-rw-r--r--app/assets/javascripts/issues/service_desk/utils.js (renamed from app/assets/javascripts/service_desk/utils.js)0
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue202
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue3
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue79
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/sticky_header.vue130
-rw-r--r--app/assets/javascripts/issues/show/components/task_list_item_actions.vue17
-rw-r--r--app/assets/javascripts/issues/show/index.js190
-rw-r--r--app/assets/javascripts/issues/show/stores/index.js46
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue1
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue2
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue55
-rw-r--r--app/assets/javascripts/jira_connect/branches/index.js1
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue4
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/constants.js13
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue64
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/utils.js27
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue47
-rw-r--r--app/assets/javascripts/jobs/constants.js40
-rw-r--r--app/assets/javascripts/labels/labels_select.js6
-rw-r--r--app/assets/javascripts/lib/swagger.js2
-rw-r--r--app/assets/javascripts/lib/utils/array_utility.js15
-rw-r--r--app/assets/javascripts/lib/utils/breadcrumbs.js28
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/constants.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js27
-rw-r--r--app/assets/javascripts/lib/utils/datetime_range.js311
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js6
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js6
-rw-r--r--app/assets/javascripts/lib/utils/secret_detection.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js3
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js18
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js8
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js5
-rw-r--r--app/assets/javascripts/locale/ensure_single_line.cjs10
-rw-r--r--app/assets/javascripts/locale/index.js64
-rw-r--r--app/assets/javascripts/locale/sprintf.js22
-rw-r--r--app/assets/javascripts/main.js3
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue3
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js10
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue20
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue13
-rw-r--r--app/assets/javascripts/merge_requests/components/header_metadata.vue (renamed from app/assets/javascripts/issuable/components/issuable_header_warnings.vue)32
-rw-r--r--app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue74
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue12
-rw-r--r--app/assets/javascripts/merge_requests/index.js19
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue45
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue13
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue3
-rw-r--r--app/assets/javascripts/notes/components/mr_discussion_filter.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue14
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue2
-rw-r--r--app/assets/javascripts/notes/components/sidebar_subscription.vue2
-rw-r--r--app/assets/javascripts/notes/stores/actions.js115
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/observability/client.js115
-rw-r--r--app/assets/javascripts/observability/mock_traces.json443
-rw-r--r--app/assets/javascripts/organizations/constants.js4
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue21
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue43
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue46
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/constants.js2
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/index.js21
-rw-r--r--app/assets/javascripts/organizations/mock_data.js258
-rw-r--r--app/assets/javascripts/organizations/shared/components/groups_view.vue82
-rw-r--r--app/assets/javascripts/organizations/shared/components/projects_view.vue86
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql)0
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql)0
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js (renamed from app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js)6
-rw-r--r--app/assets/javascripts/organizations/shared/utils.js (renamed from app/assets/javascripts/organizations/groups_and_projects/utils.js)9
-rw-r--r--app/assets/javascripts/organizations/show/components/app.vue37
-rw-r--r--app/assets/javascripts/organizations/show/components/association_count_card.vue54
-rw-r--r--app/assets/javascripts/organizations/show/components/association_counts.vue71
-rw-r--r--app/assets/javascripts/organizations/show/components/groups_and_projects.vue110
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_avatar.vue71
-rw-r--r--app/assets/javascripts/organizations/show/constants.js1
-rw-r--r--app/assets/javascripts/organizations/show/index.js63
-rw-r--r--app/assets/javascripts/organizations/show/utils.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue58
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/index.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/router.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js24
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue77
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue41
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/utils.js24
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js26
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js38
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js7
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql5
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js45
-rw-r--r--app/assets/javascripts/pages/dashboard/groups/index/index.js3
-rw-r--r--app/assets/javascripts/pages/explore/groups/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue11
-rw-r--r--app/assets/javascripts/pages/organizations/organizations/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/jobs/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js1
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js37
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js18
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js74
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue62
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js94
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/tracing/index/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/tracing/show/index.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue3
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/pipelines/mixins/stage_column_mixin.js14
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue15
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue5
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue156
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js2
-rw-r--r--app/assets/javascripts/projects/project_star_button.js46
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js611
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue126
-rw-r--r--app/assets/javascripts/projects/settings/constants.js7
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js25
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_form.vue33
-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/custom_email_constants.js2
-rw-r--r--app/assets/javascripts/projects/star.js43
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js101
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js141
-rw-r--r--app/assets/javascripts/protected_tags/constants.js4
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js71
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js115
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue113
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js46
-rw-r--r--app/assets/javascripts/related_issues/components/add_issuable_form.vue3
-rw-r--r--app/assets/javascripts/related_issues/index.js5
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue141
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue22
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue7
-rw-r--r--app/assets/javascripts/repository/constants.js8
-rw-r--r--app/assets/javascripts/rest_api.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js15
-rw-r--r--app/assets/javascripts/run_modules.js9
-rw-r--r--app/assets/javascripts/search/index.js3
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue81
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/data.js5
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/index.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/blobs_filters.vue22
-rw-r--r--app/assets/javascripts/search/sidebar/components/commits_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/components/filters_template.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue21
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue33
-rw-r--r--app/assets/javascripts/search/sidebar/components/notes_filters.vue18
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue3
-rw-r--r--app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue61
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js6
-rw-r--r--app/assets/javascripts/search/sidebar/index.js6
-rw-r--r--app/assets/javascripts/search/store/actions.js2
-rw-r--r--app/assets/javascripts/search/store/mutations.js2
-rw-r--r--app/assets/javascripts/search/store/state.js3
-rw-r--r--app/assets/javascripts/search/store/utils.js3
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue13
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js4
-rw-r--r--app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue127
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue7
-rw-r--r--app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql8
-rw-r--r--app/assets/javascripts/security_configuration/index.js2
-rw-r--r--app/assets/javascripts/security_configuration/utils.js5
-rw-r--r--app/assets/javascripts/sentry/index.js35
-rw-r--r--app/assets/javascripts/sentry/init_sentry.js77
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js4
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js31
-rw-r--r--app/assets/javascripts/settings_panels.js12
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue27
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue7
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue24
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/constants.js268
-rw-r--r--app/assets/javascripts/sidebar/queries/constants.js291
-rw-r--r--app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql9
-rw-r--r--app/assets/javascripts/silent_mode_settings/components/app.vue70
-rw-r--r--app/assets/javascripts/silent_mode_settings/index.js27
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue39
-rw-r--r--app/assets/javascripts/snippets/utils/blob.js3
-rw-r--r--app/assets/javascripts/super_sidebar/components/brand_logo.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_header.vue56
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher.vue209
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue43
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue78
-rw-r--r--app/assets/javascripts/super_sidebar/components/flyout_menu.vue129
-rw-r--r--app/assets/javascripts/super_sidebar/components/frequent_items_list.vue106
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/command_palette_items.vue15
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js10
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js35
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue8
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/global_search_default_places.vue20
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/store/getters.js13
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/utils.js13
-rw-r--r--app/assets/javascripts/super_sidebar/components/groups_list.vue81
-rw-r--r--app/assets/javascripts/super_sidebar/components/items_list.vue44
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue148
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue30
-rw-r--r--app/assets/javascripts/super_sidebar/components/projects_list.vue82
-rw-r--r--app/assets/javascripts/super_sidebar/components/search_results.vue99
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue126
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue16
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue95
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue26
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue57
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue14
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_name_group.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js10
-rw-r--r--app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql24
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js9
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js3
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js64
-rw-r--r--app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql8
-rw-r--r--app/assets/javascripts/time_tracking/components/timelogs_app.vue28
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue10
-rw-r--r--app/assets/javascripts/tracing/components/tracing_details.vue90
-rw-r--r--app/assets/javascripts/tracing/components/tracing_empty_state.vue35
-rw-r--r--app/assets/javascripts/tracing/components/tracing_list.vue125
-rw-r--r--app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue87
-rw-r--r--app/assets/javascripts/tracing/components/tracing_table_list.vue101
-rw-r--r--app/assets/javascripts/tracing/details_index.vue49
-rw-r--r--app/assets/javascripts/tracing/filters.js104
-rw-r--r--app/assets/javascripts/tracing/list_index.vue37
-rw-r--r--app/assets/javascripts/tracking/constants.js8
-rw-r--r--app/assets/javascripts/tracking/dispatch_snowplow_event.js8
-rw-r--r--app/assets/javascripts/tracking/index.js1
-rw-r--r--app/assets/javascripts/tracking/internal_events.js48
-rw-r--r--app/assets/javascripts/tracking/tracker.js8
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js64
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue80
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue136
-rw-r--r--app/assets/javascripts/user_lists/components/user_list.vue2
-rw-r--r--app/assets/javascripts/users_select/index.js1047
-rw-r--r--app/assets/javascripts/visibility_level/constants.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue55
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js189
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue313
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/confidentiality_badge.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue283
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js91
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/utils.js19
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue203
-rw-r--r--app/assets/javascripts/vue_shared/components/incidents/utils.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/constants.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js44
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field_view.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue49
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/split_button.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue6
-rw-r--r--app/assets/javascripts/vue_shared/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue56
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue11
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue3
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue24
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue44
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js3
-rw-r--r--app/assets/javascripts/webpack.js3
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue35
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue150
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_discussion.vue6
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue4
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue11
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue3
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue25
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue145
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue80
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_created_updated.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue3
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue94
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue61
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue185
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_badge.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js27
-rw-r--r--app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/components/work_items_list_app.vue25
-rw-r--r--app/assets/javascripts/work_items/list/index.js15
-rw-r--r--app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql38
-rw-r--r--app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql5
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue11
-rw-r--r--app/assets/javascripts/work_items/utils.js23
-rw-r--r--app/assets/javascripts/work_items_hierarchy/hierarchy_util.js3
-rw-r--r--app/assets/stylesheets/disable_animations.scss5
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss17
-rw-r--r--app/assets/stylesheets/framework/filters.scss16
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/framework/job_log.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss12
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss35
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss103
-rw-r--r--app/assets/stylesheets/framework/variables.scss148
-rw-r--r--app/assets/stylesheets/page_bundles/admin/jobs_index.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss77
-rw-r--r--app/assets/stylesheets/page_bundles/incidents.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss12
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss7
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline_schedules.scss82
-rw-r--r--app/assets/stylesheets/page_bundles/profiles/preferences.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/projects_usage_quotas.scss19
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss10
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss9
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss2
-rw-r--r--app/assets/stylesheets/utilities.scss13
879 files changed, 10499 insertions, 10653 deletions
diff --git a/app/assets/images/auth_buttons/salesforce_64.png b/app/assets/images/auth_buttons/salesforce_64.png
deleted file mode 100644
index b562e09c20f..00000000000
--- a/app/assets/images/auth_buttons/salesforce_64.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
index 85b3c994e02..9a7296b6b1f 100644
--- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -33,7 +33,7 @@ export default {
emptyField: __('Never'),
expired: __('Expired'),
modalMessage: __(
- 'Are you sure you want to revoke this %{accessTokenType}? This action cannot be undone.',
+ 'Are you sure you want to revoke the %{accessTokenType} "%{tokenName}"? This action cannot be undone.',
),
revokeButton: __('Revoke'),
tokenValidity: __('Token valid until revoked'),
@@ -72,11 +72,6 @@ export default {
return FIELDS.filter(({ key }) => !ignoredFields.includes(key));
},
- modalMessage() {
- return sprintf(this.$options.i18n.modalMessage, {
- accessTokenType: this.accessTokenType,
- });
- },
showPagination() {
return this.activeAccessTokens.length > PAGE_SIZE;
},
@@ -87,6 +82,12 @@ export default {
this.activeAccessTokens = convertObjectPropsToCamelCase(activeAccessTokens, { deep: true });
this.currentPage = INITIAL_PAGE;
},
+ modalMessage(tokenName) {
+ return sprintf(this.$options.i18n.modalMessage, {
+ accessTokenType: this.accessTokenType,
+ tokenName,
+ });
+ },
sortingChanged(aRow, bRow, key) {
if (['createdAt', 'lastUsedAt', 'expiresAt'].includes(key)) {
// Transform `null` value to the latest possible date
@@ -149,13 +150,13 @@ export default {
}}</span>
</template>
- <template #cell(action)="{ item: { revokePath } }">
+ <template #cell(action)="{ item: { name, revokePath } }">
<gl-button
v-if="revokePath"
category="tertiary"
:title="$options.i18n.revokeButton"
:aria-label="$options.i18n.revokeButton"
- :data-confirm="modalMessage"
+ :data-confirm="modalMessage(name)"
data-confirm-btn-variant="danger"
data-qa-selector="revoke_button"
data-method="put"
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
index c1ec46cfc50..9bcdcec8b78 100644
--- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -267,7 +267,8 @@ export default {
});
}
});
- } else if (this.uniqueCommits.length > 0) {
+ }
+ if (this.uniqueCommits.length > 0) {
return this.createContextCommits({ commits: this.uniqueCommits, forceReload: true });
}
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 1490d7e64f5..3c46de7c2be 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
@@ -1,9 +1,12 @@
<script>
import { GlAlert } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportHeader from './report_header.vue';
import UserDetails from './user_details.vue';
+import ReportDetails from './report_details.vue';
import ReportedContent from './reported_content.vue';
-import HistoryItems from './history_items.vue';
+import ActivityEventsList from './activity_events_list.vue';
+import ActivityHistoryItem from './activity_history_item.vue';
const alertDefaults = {
visible: false,
@@ -17,9 +20,12 @@ export default {
GlAlert,
ReportHeader,
UserDetails,
+ ReportDetails,
ReportedContent,
- HistoryItems,
+ ActivityEventsList,
+ ActivityHistoryItem,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
abuseReport: {
type: Object,
@@ -31,6 +37,11 @@ export default {
alert: { ...alertDefaults },
};
},
+ computed: {
+ similarOpenReports() {
+ return this.abuseReport.user?.similarOpenReports || [];
+ },
+ },
methods: {
showAlert(variant, message) {
this.alert.visible = true;
@@ -49,6 +60,7 @@ export default {
<gl-alert v-if="alert.visible" :variant="alert.variant" class="gl-mt-4" @dismiss="closeAlert">{{
alert.message
}}</gl-alert>
+
<report-header
v-if="abuseReport.user"
:user="abuseReport.user"
@@ -56,7 +68,33 @@ export default {
@showAlert="showAlert"
/>
<user-details v-if="abuseReport.user" :user="abuseReport.user" />
- <reported-content :report="abuseReport.report" :reporter="abuseReport.reporter" />
- <history-items :report="abuseReport.report" :reporter="abuseReport.reporter" />
+
+ <report-details
+ v-if="glFeatures.abuseReportLabels"
+ :report-id="abuseReport.report.globalId"
+ class="gl-mt-6"
+ />
+
+ <reported-content :report="abuseReport.report" data-testid="reported-content" />
+
+ <div
+ v-for="report in similarOpenReports"
+ :key="report.id"
+ data-testid="reported-content-similar-open-reports"
+ >
+ <reported-content :report="report" />
+ </div>
+
+ <activity-events-list>
+ <template #history-items>
+ <activity-history-item :report="abuseReport.report" data-testid="activity" />
+ <activity-history-item
+ v-for="report in similarOpenReports"
+ :key="report.id"
+ :report="report"
+ data-testid="activity-similar-open-reports"
+ />
+ </template>
+ </activity-events-list>
</section>
</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
new file mode 100644
index 00000000000..8c4c1da28b8
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_events_list.vue
@@ -0,0 +1,19 @@
+<script>
+import { HISTORY_ITEMS_I18N } from '../constants';
+
+export default {
+ name: 'ActivityEventsList',
+ i18n: HISTORY_ITEMS_I18N,
+};
+</script>
+
+<template>
+ <!-- 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>
+ <ul class="timeline main-notes-list notes">
+ <slot name="history-items"></slot>
+ </ul>
+ </section>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue b/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
new file mode 100644
index 00000000000..5962203c382
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
+import { HISTORY_ITEMS_I18N } from '../constants';
+
+export default {
+ name: 'ActivityHistoryItem',
+ components: {
+ GlSprintf,
+ TimeAgoTooltip,
+ HistoryItem,
+ },
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ reporter() {
+ return this.report.reporter;
+ },
+ reporterName() {
+ return this.reporter?.name || this.$options.i18n.deletedReporter;
+ },
+ },
+ i18n: HISTORY_ITEMS_I18N,
+};
+</script>
+
+<template>
+ <history-item icon="warning">
+ <div class="gl-display-flex gl-xs-flex-direction-column">
+ <gl-sprintf :message="$options.i18n.reportedByForCategory">
+ <template #name>{{ reporterName }}</template>
+ <template #category>{{ report.category }}</template>
+ </gl-sprintf>
+ <time-ago-tooltip :time="report.reportedAt" class="gl-text-secondary gl-sm-ml-3" />
+ </div>
+ </history-item>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
new file mode 100644
index 00000000000..f5b075cb9af
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report.query.graphql
@@ -0,0 +1,13 @@
+query abuseReportQuery($id: AbuseReportID!) {
+ abuseReport(id: $id) {
+ labels {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
new file mode 100644
index 00000000000..4e724b4db2c
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/graphql/abuse_report_labels.query.graphql
@@ -0,0 +1,11 @@
+query abuseReportLabelsQuery($searchTerm: String) {
+ labels: abuseReportLabels(searchTerm: $searchTerm) {
+ nodes {
+ id
+ title
+ description
+ color
+ textColor
+ }
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql b/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
new file mode 100644
index 00000000000..0781b8e634b
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/graphql/create_abuse_report_label.mutation.graphql
@@ -0,0 +1,10 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+mutation createAbuseReportLabel($title: String!, $color: String) {
+ labelCreate: abuseReportLabelCreate(input: { title: $title, color: $color }) {
+ label {
+ ...Label
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/admin/abuse_report/components/history_items.vue b/app/assets/javascripts/admin/abuse_report/components/history_items.vue
deleted file mode 100644
index 28b66db84a2..00000000000
--- a/app/assets/javascripts/admin/abuse_report/components/history_items.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-import { GlSprintf } from '@gitlab/ui';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
-import { HISTORY_ITEMS_I18N } from '../constants';
-
-export default {
- name: 'HistoryItems',
- components: {
- GlSprintf,
- TimeAgoTooltip,
- HistoryItem,
- },
- props: {
- report: {
- type: Object,
- required: true,
- },
- reporter: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- reporterName() {
- return this.reporter?.name || this.$options.i18n.deletedReporter;
- },
- },
- i18n: HISTORY_ITEMS_I18N,
-};
-</script>
-
-<template>
- <!-- 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-size-h1 gl-mt-0 gl-mb-2">{{ $options.i18n.activity }}</h2>
- <ul class="timeline main-notes-list notes">
- <history-item icon="warning">
- <div class="gl-display-flex gl-xs-flex-direction-column">
- <gl-sprintf :message="$options.i18n.reportedByForCategory">
- <template #name>{{ reporterName }}</template>
- <template #category>{{ report.category }}</template>
- </gl-sprintf>
- <time-ago-tooltip :time="report.reportedAt" class="gl-text-secondary gl-sm-ml-3" />
- </div>
- </history-item>
- </ul>
- </section>
-</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/labels_select.vue b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
new file mode 100644
index 00000000000..747c9a1a947
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/labels_select.vue
@@ -0,0 +1,235 @@
+<script>
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { __, s__, sprintf } from '~/locale';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
+import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
+import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue';
+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';
+
+export default {
+ components: {
+ DropdownWidget,
+ GlButton,
+ GlLoadingIcon,
+ LabelItem,
+ DropdownValue,
+ DropdownContentsCreateView,
+ DropdownHeader,
+ DropdownFooter,
+ },
+ inject: ['updatePath', 'listPath'],
+ props: {
+ report: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ labels: [],
+ selected: this.report.labels,
+ initialLoading: true,
+ isEditing: false,
+ isUpdating: false,
+ showCreateView: false,
+ };
+ },
+ apollo: {
+ labels: {
+ query() {
+ return abuseReportLabelsQuery;
+ },
+ variables() {
+ return { searchTerm: this.search };
+ },
+ skip() {
+ return !this.isEditing;
+ },
+ update(data) {
+ return data.labels?.nodes;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.searchError });
+ },
+ },
+ },
+ computed: {
+ isLabelsEmpty() {
+ return this.selected.length === 0;
+ },
+ selectedLabelIds() {
+ return this.selected.map((label) => label.id);
+ },
+ isLoading() {
+ return this.$apollo.queries.labels.loading;
+ },
+ selectText() {
+ if (!this.selected.length) {
+ return this.$options.i18n.labelsListTitle;
+ }
+ if (this.selected.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.selected[0].title,
+ remainingLabelCount: this.selected.length - 1,
+ });
+ }
+ return this.selected[0].title;
+ },
+ },
+ watch: {
+ report({ labels }) {
+ this.selected = labels;
+ this.initialLoading = false;
+ },
+ },
+ created() {
+ const setSearch = (search) => {
+ this.search = search;
+ };
+ this.debouncedSetSearch = debounce(setSearch, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ toggleEdit() {
+ return this.isEditing ? this.hideDropdown() : this.showDropdown();
+ },
+ showDropdown() {
+ this.isEditing = true;
+ this.$refs.editDropdown.showDropdown();
+ },
+ hideDropdown() {
+ this.saveSelectedLabels();
+ this.isEditing = false;
+ },
+ saveSelectedLabels() {
+ this.isUpdating = true;
+
+ axios
+ .put(this.updatePath, { label_ids: this.selectedLabelIds })
+ .catch((error) => {
+ createAlert({
+ message: __('An error occurred while updating labels.'),
+ captureError: true,
+ error,
+ });
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ isLabelSelected(label) {
+ return this.selectedLabelIds.includes(label.id);
+ },
+ filterSelected(id) {
+ return this.selected.filter(({ id: labelId }) => labelId !== id);
+ },
+ toggleLabelSelection(label) {
+ this.selected = this.isLabelSelected(label)
+ ? this.filterSelected(label.id)
+ : [...this.selected, label];
+ },
+ removeLabel(labelId) {
+ this.selected = this.filterSelected(labelId);
+ this.saveSelectedLabels();
+ },
+ toggleCreateView() {
+ this.showCreateView = !this.showCreateView;
+ },
+ onLabelCreated(label) {
+ this.toggleLabelSelection(label);
+ this.toggleCreateView();
+ },
+ },
+ i18n: {
+ label: __('Labels'),
+ noLabels: __('None'),
+ labelsListTitle: __('Assign labels'),
+ searchError: __('An error occurred while searching for labels, please try again.'),
+ edit: __('Edit'),
+ },
+};
+</script>
+<template>
+ <div class="labels-select-wrapper">
+ <div class="gl-display-flex gl-align-items-center gl-gap-3 gl-mb-2">
+ <span>{{ $options.i18n.label }}</span>
+ <gl-loading-icon v-if="initialLoading" size="sm" inline class="gl-ml-2" />
+ <gl-button
+ category="tertiary"
+ size="small"
+ :disabled="isUpdating || initialLoading"
+ class="edit-link gl-ml-auto"
+ @click="toggleEdit"
+ >
+ {{ $options.i18n.edit }}
+ </gl-button>
+ </div>
+ <div class="gl-text-gray-500 gl-mb-2" data-testid="selected-labels">
+ <template v-if="isLabelsEmpty">{{ $options.i18n.noLabels }}</template>
+ <dropdown-value
+ v-else
+ :disable-labels="isLoading"
+ :selected-labels="selected"
+ :allow-label-remove="!isUpdating"
+ :labels-filter-base-path="listPath"
+ :labels-filter-param="'label_name'"
+ @onLabelRemove="removeLabel"
+ />
+ </div>
+
+ <dropdown-widget
+ v-show="isEditing"
+ ref="editDropdown"
+ :select-text="selectText"
+ :options="labels"
+ :is-loading="isLoading"
+ :selected="selected"
+ :search-term="search"
+ :allow-multiselect="true"
+ :no-options-text="__('No labels found')"
+ @hide="hideDropdown"
+ @set-option="toggleLabelSelection"
+ @set-search="debouncedSetSearch"
+ >
+ <template #header>
+ <dropdown-header
+ ref="header"
+ :search-key="search"
+ labels-create-title=""
+ :labels-list-title="$options.i18n.labelsListTitle"
+ :show-dropdown-contents-create-view="showCreateView"
+ @toggleDropdownContentsCreateView="toggleCreateView"
+ @closeDropdown="hideDropdown"
+ @input="debouncedSetSearch"
+ />
+ </template>
+ <template #item="{ item }">
+ <label-item v-if="item" :label="item" />
+ </template>
+ <template v-if="showCreateView" #default>
+ <dropdown-contents-create-view
+ attr-workspace-path=""
+ full-path=""
+ label-create-type=""
+ workspace-type="abuseReport"
+ @hideCreateView="toggleCreateView"
+ @labelCreated="onLabelCreated"
+ />
+ </template>
+ <template #footer>
+ <dropdown-footer
+ v-if="!showCreateView"
+ :footer-create-label-title="__('Create label')"
+ @toggleDropdownContentsCreateView="toggleCreateView"
+ />
+ </template>
+ </dropdown-widget>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
index 92478e10289..560d733c10c 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
@@ -95,12 +95,8 @@ export default {
return;
}
- // TODO: In 16.4 use moderateUserPath without falling back to using updatePath
- // See https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/167?work_item_iid=443
- const { moderateUserPath, updatePath } = this.report;
- const path = moderateUserPath || updatePath;
-
- axios.put(path, this.form).then(this.handleResponse).catch(this.handleError);
+ const { moderateUserPath } = this.report;
+ axios.put(moderateUserPath, this.form).then(this.handleResponse).catch(this.handleError);
},
handleResponse({ data }) {
this.toggleActionsDrawer();
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_details.vue b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
new file mode 100644
index 00000000000..10e1dca7f91
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/report_details.vue
@@ -0,0 +1,49 @@
+<script>
+import { __ } from '~/locale';
+import { createAlert } from '~/alert';
+import LabelsSelect from './labels_select.vue';
+import abuseReportQuery from './graphql/abuse_report.query.graphql';
+
+export default {
+ name: 'ReportDetails',
+ components: {
+ LabelsSelect,
+ },
+ props: {
+ reportId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ report: { labels: [] },
+ };
+ },
+ apollo: {
+ report: {
+ query() {
+ return abuseReportQuery;
+ },
+ variables() {
+ return { id: this.reportId };
+ },
+ update({ abuseReport }) {
+ return {
+ labels: abuseReport.labels?.nodes,
+ };
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ i18n: {
+ fetchError: __('An error occurred while fetching labels, please try again.'),
+ },
+};
+</script>
+
+<template>
+ <labels-select :report="report" />
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_header.vue b/app/assets/javascripts/admin/abuse_report/components/report_header.vue
index 624dcd47650..90c1943cb27 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_header.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_header.vue
@@ -32,9 +32,6 @@ export default {
isOpen() {
return this.state === STATUS_OPEN;
},
- badgeClass() {
- return this.isOpen ? 'issuable-status-badge-open' : 'issuable-status-badge-closed';
- },
badgeVariant() {
return this.isOpen ? 'success' : 'info';
},
@@ -58,21 +55,16 @@ export default {
<header
class="gl-py-4 gl-border-b gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
>
- <div class="gl-display-flex gl-align-items-center">
- <gl-badge
- class="issuable-status-badge gl-mr-3"
- :class="badgeClass"
- :variant="badgeVariant"
- :aria-label="badgeText"
- >
+ <div class="gl-display-flex gl-align-items-center gl-gap-3">
+ <gl-badge :variant="badgeVariant" :aria-label="badgeText">
<gl-icon :name="badgeIcon" class="gl-badge-icon" />
<span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
</gl-badge>
<gl-avatar :size="48" :src="user.avatarUrl" />
- <h1 class="gl-font-size-h-display gl-my-0 gl-ml-3">
+ <h1 class="gl-font-size-h-display gl-my-0">
{{ user.name }}
</h1>
- <gl-link :href="user.path" class="gl-ml-3"> @{{ user.username }} </gl-link>
+ <gl-link :href="user.path"> @{{ user.username }} </gl-link>
</div>
<nav
class="gl-display-flex gl-sm-align-items-center gl-mt-4 gl-sm-mt-0 gl-xs-flex-direction-column"
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 f4f0fcac58f..84d6f25ac05 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -26,11 +26,6 @@ export default {
type: Object,
required: true,
},
- reporter: {
- type: Object,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -38,6 +33,9 @@ export default {
};
},
computed: {
+ reporter() {
+ return this.report.reporter;
+ },
reporterName() {
return this.reporter?.name || this.$options.i18n.deletedReporter;
},
@@ -67,11 +65,12 @@ export default {
<template>
<div class="gl-pt-6">
<div
- class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
+ 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-size-h1 gl-mt-0 gl-mb-2">
+ <h2 class="gl-font-lg gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
+
<div
class="gl-display-flex gl-align-items-stretch gl-xs-flex-direction-column gl-mt-3 gl-sm-mt-0"
>
diff --git a/app/assets/javascripts/admin/abuse_report/components/user_details.vue b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
index 3dc03a8748f..fe0add1ba8d 100644
--- a/app/assets/javascripts/admin/abuse_report/components/user_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
@@ -39,19 +39,27 @@ export default {
<template>
<div class="gl-mt-6">
- <user-detail data-testid="createdAt" :label="$options.i18n.createdAt">
+ <user-detail data-testid="created-at" :label="$options.i18n.createdAt">
<time-ago-tooltip :time="user.createdAt" />
</user-detail>
+
<user-detail data-testid="email" :label="$options.i18n.email">
<gl-link :href="`mailto:${user.email}`">{{ user.email }}</gl-link>
</user-detail>
+
<user-detail data-testid="plan" :label="$options.i18n.plan" :value="user.plan" />
+
<user-detail
data-testid="verification"
:label="$options.i18n.verification"
:value="verificationState"
/>
- <user-detail v-if="user.creditCard" data-testid="creditCard" :label="$options.i18n.creditCard">
+
+ <user-detail
+ v-if="user.creditCard"
+ data-testid="credit-card-verification"
+ :label="$options.i18n.creditCard"
+ >
<gl-sprintf :message="$options.i18n.registeredWith">
<template #name>{{ user.creditCard.name }}</template>
</gl-sprintf>
@@ -65,17 +73,18 @@ export default {
</template>
</gl-sprintf>
</user-detail>
+
<user-detail
- v-if="user.otherReports.length"
- data-testid="otherReports"
- :label="$options.i18n.otherReports"
+ v-if="user.pastClosedReports.length"
+ data-testid="past-closed-reports"
+ :label="$options.i18n.pastReports"
>
<div
- v-for="(report, index) in user.otherReports"
+ v-for="(report, index) in user.pastClosedReports"
:key="index"
- :data-testid="`other-report-${index}`"
+ :data-testid="`past-report-${index}`"
>
- <gl-sprintf :message="$options.i18n.otherReport">
+ <gl-sprintf :message="$options.i18n.reportedFor">
<template #reportLink="{ content }">
<gl-link :href="report.reportPath">{{ content }}</gl-link>
</template>
@@ -86,28 +95,33 @@ export default {
</gl-sprintf>
</div>
</user-detail>
+
<user-detail
- data-testid="normalLocation"
+ data-testid="normal-location"
:label="$options.i18n.normalLocation"
:value="user.mostUsedIp || user.lastSignInIp"
/>
+
<user-detail
- data-testid="lastSignInIp"
+ data-testid="last-sign-in-ip"
:label="$options.i18n.lastSignInIp"
:value="user.lastSignInIp"
/>
+
<user-detail
- data-testid="snippets"
+ data-testid="user-snippets-count"
:label="$options.i18n.snippets"
:value="$options.i18n.snippetsCount(user.snippetsCount)"
/>
+
<user-detail
- data-testid="groups"
+ data-testid="user-groups-count"
:label="$options.i18n.groups"
:value="$options.i18n.groupsCount(user.groupsCount)"
/>
+
<user-detail
- data-testid="notes"
+ data-testid="user-notes-count"
:label="$options.i18n.notes"
:value="$options.i18n.notesCount(user.notesCount)"
/>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index b290581598a..6cae6b24f20 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -58,7 +58,7 @@ export const USER_DETAILS_I18N = {
plan: s__('AbuseReport|Tier'),
verification: s__('AbuseReport|Verification'),
creditCard: s__('AbuseReport|Credit card'),
- otherReports: s__('AbuseReport|Abuse reports'),
+ pastReports: s__('AbuseReport|Past abuse reports'),
normalLocation: s__('AbuseReport|Normal location'),
lastSignInIp: s__('AbuseReport|Last login'),
snippets: s__('AbuseReport|Snippets'),
@@ -72,7 +72,7 @@ export const USER_DETAILS_I18N = {
phone: s__('AbuseReport|Phone'),
creditCard: s__('AbuseReport|Credit card'),
},
- otherReport: s__(
+ reportedFor: s__(
'AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}.',
),
registeredWith: s__('AbuseReport|Registered with name %{name}.'),
diff --git a/app/assets/javascripts/admin/abuse_report/index.js b/app/assets/javascripts/admin/abuse_report/index.js
index 8ff3e690127..c2117130d26 100644
--- a/app/assets/javascripts/admin/abuse_report/index.js
+++ b/app/assets/javascripts/admin/abuse_report/index.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { defaultClient } from '~/graphql_shared/issuable_client';
import AbuseReportApp from './components/abuse_report_app.vue';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
export const initAbuseReportApp = () => {
const el = document.querySelector('#js-abuse-reports-detail-view');
@@ -9,14 +17,22 @@ export const initAbuseReportApp = () => {
return null;
}
- const { abuseReportData } = el.dataset;
+ const { abuseReportData, abuseReportsListPath } = el.dataset;
const abuseReport = convertObjectPropsToCamelCase(JSON.parse(abuseReportData), {
deep: true,
});
return new Vue({
el,
+ apolloProvider,
name: 'AbuseReportAppRoot',
+ provide: {
+ allowScopedLabels: false,
+ updatePath: abuseReport.report.updatePath,
+ listPath: abuseReportsListPath,
+ labelsManagePath: '',
+ allowLabelCreate: true,
+ },
render: (createElement) =>
createElement(AbuseReportApp, {
props: {
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
index f24e491a745..b9fef57c2a2 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
@@ -1,7 +1,7 @@
<script>
-import { GlLink } from '@gitlab/ui';
+import { GlLabel, GlLink } from '@gitlab/ui';
import { getTimeago } from '~/lib/utils/datetime_utility';
-import { queryToObject } from '~/lib/utils/url_utility';
+import { mergeUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { SORT_UPDATED_AT } from '../constants';
@@ -10,6 +10,7 @@ import AbuseCategory from './abuse_category.vue';
export default {
name: 'AbuseReportRow',
components: {
+ GlLabel,
GlLink,
ListItem,
AbuseCategory,
@@ -53,6 +54,11 @@ export default {
});
},
},
+ methods: {
+ labelTarget(labelName) {
+ return mergeUrlParams({ 'label_name[]': labelName }, window.location.href);
+ },
+ },
};
</script>
@@ -68,7 +74,16 @@ export default {
</gl-link>
</template>
<template #left-secondary>
- <abuse-category :category="report.category" class="gl-mt-2 gl-mb-3" />
+ <abuse-category :category="report.category" class="gl-mr-2" />
+ <gl-label
+ v-for="label in report.labels"
+ :key="label.id"
+ class="gl-mr-2"
+ size="sm"
+ :background-color="label.color"
+ :title="label.title"
+ :target="labelTarget(label.title)"
+ />
</template>
<template #right-secondary>
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 109df943c42..2c555aca3c0 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -234,7 +234,8 @@ export default {
initialTarget() {
if (this.targetAccessLevels.length > 0) {
return TARGET_ROLES;
- } else if (this.targetPath !== '') {
+ }
+ if (this.targetPath !== '') {
return TARGET_ALL_MATCHING_PATH;
}
return TARGET_ALL;
diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue
index 5b13bd177ae..bcd17570b95 100644
--- a/app/assets/javascripts/admin/users/components/actions/approve.vue
+++ b/app/assets/javascripts/admin/users/components/actions/approve.vue
@@ -44,7 +44,7 @@ export default {
},
actionPrimary: {
text: I18N_USER_ACTIONS.approve,
- attributes: { variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' },
+ attributes: { variant: 'confirm', 'data-testid': 'approve-user-confirm-button' },
},
messageHtml,
},
@@ -55,7 +55,7 @@ export default {
</script>
<template>
- <gl-disclosure-dropdown-item data-qa-selector="approve_user_button" @action="onClick">
+ <gl-disclosure-dropdown-item @action="onClick">
<template #list-item>
<slot></slot>
</template>
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index 38c7d3f9b90..a9482d479b6 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -116,8 +116,7 @@ export default {
category="tertiary"
:toggle-text="$options.i18n.userAdministration"
text-sr-only
- data-testid="dropdown-toggle"
- data-qa-selector="user_actions_dropdown_toggle"
+ data-testid="user-actions-dropdown-toggle"
:data-qa-username="user.username"
no-caret
>
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 170bd6895aa..9e57b834c88 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -313,7 +313,7 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
- data-qa-selector="alert_table_container"
+ data-testid="alert-table-container"
:items="
alerts
? alerts.list
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
index b9e37b9ede7..6baa431f2d9 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
@@ -72,7 +72,7 @@ export default {
</p>
<form ref="settingsForm" @submit.prevent="updateAlertsIntegrationSettings">
<gl-form-group class="gl-pl-0">
- <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_incident_checkbox">
+ <gl-form-checkbox v-model="createIssueEnabled" data-testid="create-incident-checkbox">
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
@@ -93,14 +93,14 @@ export default {
v-model="issueTemplate"
:items="templates"
block
- data-qa-selector="incident_templates_dropdown"
+ data-testid="incident-templates-dropdown"
/>
</gl-form-group>
<gl-form-group class="gl-pl-0 gl-mb-5">
<gl-form-checkbox
v-model="sendEmailEnabled"
- data-qa-selector="enable_email_notification_checkbox"
+ data-testid="enable-email-notification-checkbox"
>
<span>{{ $options.i18n.sendEmail.label }}</span>
</gl-form-checkbox>
@@ -112,7 +112,7 @@ export default {
</gl-form-group>
<gl-button
ref="submitBtn"
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
:disabled="loading"
variant="confirm"
type="submit"
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 56740e436ca..fb872243e5e 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -406,7 +406,7 @@ export default {
v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
- data-qa-selector="integration_type_dropdown"
+ data-testid="integration-type-dropdown"
:options="integrationTypesOptions"
autofocus
/>
@@ -439,7 +439,7 @@ export default {
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
- data-qa-selector="integration_name_field"
+ data-testid="integration-name-field"
@input="validateName"
/>
</gl-form-group>
@@ -462,7 +462,7 @@ export default {
v-model="integrationForm.active"
:is-loading="loading"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
- data-qa-selector="active_toggle_container"
+ data-testid="active-toggle-container"
class="gl-mt-4 gl-font-weight-normal"
/>
</gl-form-group>
@@ -552,7 +552,7 @@ export default {
variant="confirm"
category="secondary"
class="gl-ml-3 js-no-auto-disable"
- data-qa-selector="save_and_create_alert_button"
+ data-testid="save-and-create-alert-button"
@click="submit(true)"
>
{{ $options.i18n.saveAndTestIntegration }}
@@ -654,7 +654,7 @@ export default {
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
- data-qa-selector="test_payload_field"
+ data-testid="test-payload-field"
@input="validateJson(false)"
/>
</gl-form-group>
@@ -666,7 +666,6 @@ export default {
data-testid="send-test-alert"
variant="confirm"
class="js-no-auto-disable"
- data-qa-selector="send_test_alert_button"
@click="isFormDirty ? null : sendTestAlert()"
>
{{ $options.i18n.send }}
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index e4fc37f9760..e735ee466ad 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -385,8 +385,7 @@ export default {
<gl-button
v-if="canAddIntegration && !formVisible"
size="small"
- data-testid="add-integration-btn"
- data-qa-selector="add_integration_button"
+ data-testid="add-integration-button"
@click="setFormVisibility(true)"
>
{{ $options.i18n.addNewIntegration }}
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
index b622b0441e2..724e9c91305 100644
--- a/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
@@ -13,7 +13,8 @@ export default {
formattedStageCount() {
if (!this.stageCount) {
return '-';
- } else if (this.stageCount > 1000) {
+ }
+ if (this.stageCount > 1000) {
return sprintf(s__('ValueStreamAnalytics|%{stageCount}+ items'), {
stageCount: formatNumber(1000),
});
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index f881c924ae5..ddfc6baafa9 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -69,7 +69,8 @@ export default {
selectedProjectsLabel() {
if (this.selectedProjects.length === 1) {
return this.selectedProjects[0].name;
- } else if (this.selectedProjects.length > 1) {
+ }
+ if (this.selectedProjects.length > 1) {
return n__(
'CycleAnalytics|Project selected',
'CycleAnalytics|%d projects selected',
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 185cdaa1c99..6dfc1c609de 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -33,6 +33,7 @@ const Api = {
forkedProjectsPath: '/api/:version/projects/:id/forks',
projectLabelsPath: '/:namespace_path/:project_path/-/labels',
projectFileSchemaPath: '/:namespace_path/:project_path/-/schema/:ref/:filename',
+ projectGroupsPath: '/api/:version/projects/:id/groups.json',
projectUsersPath: '/api/:version/projects/:id/users',
projectInvitationsPath: '/api/:version/projects/:id/invitations',
projectMembersPath: '/api/:version/projects/:id/members',
@@ -177,6 +178,19 @@ const Api = {
});
},
+ projectGroups(id, options) {
+ const url = Api.buildUrl(this.projectGroupsPath).replace(':id', encodeURIComponent(id));
+
+ return axios
+ .get(url, {
+ params: {
+ ...options,
+ },
+ })
+ .then(({ data }) => {
+ return data;
+ });
+ },
/**
* @deprecated This method will be removed soon. Use the
* `getGroups` method in `~/rest_api` instead.
diff --git a/app/assets/javascripts/api/application_settings_api.js b/app/assets/javascripts/api/application_settings_api.js
new file mode 100644
index 00000000000..839636c36f4
--- /dev/null
+++ b/app/assets/javascripts/api/application_settings_api.js
@@ -0,0 +1,14 @@
+import axios from '../lib/utils/axios_utils';
+import { buildApiUrl } from './api_utils';
+
+const APPLICATION_SETTINGS_PATH = '/api/:version/application/settings';
+
+export function getApplicationSettings() {
+ const url = buildApiUrl(APPLICATION_SETTINGS_PATH);
+ return axios.get(url);
+}
+
+export function updateApplicationSettings(data) {
+ const url = buildApiUrl(APPLICATION_SETTINGS_PATH);
+ return axios.put(url, data);
+}
diff --git a/app/assets/javascripts/authentication/webauthn/error.js b/app/assets/javascripts/authentication/webauthn/error.js
index 40dbecd8bc9..ce6c79a1f11 100644
--- a/app/assets/javascripts/authentication/webauthn/error.js
+++ b/app/assets/javascripts/authentication/webauthn/error.js
@@ -14,11 +14,14 @@ export default class WebAuthnError {
message() {
if (this.errorName === 'NotSupportedError') {
return __('Your device is not compatible with GitLab. Please try another device');
- } else if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_AUTHENTICATE) {
+ }
+ if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_AUTHENTICATE) {
return __('This device has not been registered with us.');
- } else if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_REGISTER) {
+ }
+ if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_REGISTER) {
return __('This device has already been registered with us.');
- } else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
+ }
+ if (this.errorName === 'SecurityError' && this.httpsDisabled) {
return __(
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.',
);
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
index 095634340c1..e1c1bd58ee2 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js
@@ -20,7 +20,8 @@ export default () => ({
const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox');
if (checkbox?.matches('[data-inapplicable]')) {
return { state: 'inapplicable' };
- } else if (checkbox?.checked) {
+ }
+ if (checkbox?.checked) {
return { state: 'done' };
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 689f2f0898e..e8c486f6e74 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -636,6 +636,7 @@ const MR_SHORTCUTS_GROUP = {
MR_NEXT_UNRESOLVED_DISCUSSION,
MR_PREVIOUS_UNRESOLVED_DISCUSSION,
MR_COPY_SOURCE_BRANCH_NAME,
+ MR_TOGGLE_FILE_BROWSER,
],
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
index cb7c6f9f6bc..e81ceae57c0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -1,15 +1,16 @@
<script>
-import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
+import { GlModal, GlSearchBoxByType, GlLink, GlSprintf } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import { joinPaths } from '../../lib/utils/url_utility';
import { keybindingGroups } from './keybindings';
import Shortcut from './shortcut.vue';
-import ShortcutsToggle from './shortcuts_toggle.vue';
export default {
components: {
GlModal,
GlSearchBoxByType,
- ShortcutsToggle,
+ GlLink,
+ GlSprintf,
Shortcut,
},
data() {
@@ -39,6 +40,9 @@ export default {
return mapped.filter((group) => group.keybindings.length);
},
+ absoluteUserPreferencesPath() {
+ return joinPaths(gon.relative_url_root || '/', '/-/profile/preferences');
+ },
},
i18n: {
title: __(`Keyboard shortcuts`),
@@ -66,7 +70,21 @@ export default {
:aria-label="$options.i18n.search"
class="gl-w-half gl-mr-3"
/>
- <shortcuts-toggle class="gl-w-half gl-ml-3" />
+ <span>
+ <gl-sprintf
+ :message="
+ __(
+ 'Enable or disable keyboard shortcuts in your %{linkStart}user preferences%{linkEnd}.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="absoluteUserPreferencesPath">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
</div>
<div v-if="filteredKeybindings.length === 0" class="gl-px-5">
{{ $options.i18n.noMatch }}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js
index 3f3e0c51de5..22f2478c530 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.js
@@ -3,13 +3,7 @@ import 'mousetrap/plugins/pause/mousetrap-pause';
const shorcutsDisabledKey = 'shortcutsDisabled';
-export const shouldDisableShortcuts = () => {
- try {
- return localStorage.getItem(shorcutsDisabledKey) === 'true';
- } catch (e) {
- return false;
- }
-};
+export const shouldDisableShortcuts = () => !window.gon.keyboard_shortcuts_enabled;
export function enableShortcuts() {
localStorage.setItem(shorcutsDisabledKey, false);
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 30424fee46a..4e643f71c4b 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { fixTitle } from '~/tooltips';
import { getLocationHash } from '../lib/utils/url_utility';
// Toggle button. Show/hide content inside parent container.
@@ -29,9 +30,22 @@ $(() => {
$container.find('.js-toggle-content').toggle(toggleState);
}
+ function updateTitle(el, container) {
+ const $container = $(container);
+ const isExpanded = $container.data('is-expanded');
+
+ el.setAttribute('title', isExpanded ? el.dataset.collapseTitle : el.dataset.expandTitle);
+
+ fixTitle(el);
+ }
+
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'selected');
- toggleContainer($(this).closest('.js-toggle-container'));
+
+ const containerEl = this.closest('.js-toggle-container');
+
+ toggleContainer(containerEl);
+ updateTitle(this, containerEl);
const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 4e47aa99fd8..699a0491183 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -1,5 +1,8 @@
<script>
import DefaultActions from 'jh_else_ce/blob/components/blob_header_default_actions.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+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';
@@ -11,6 +14,21 @@ export default {
DefaultActions,
BlobFilepath,
TableOfContents,
+ WebIdeLink: () => import('ee_else_ce/vue_shared/components/web_ide_link.vue'),
+ },
+ apollo: {
+ currentUser: {
+ query: userInfoQuery,
+ error() {
+ this.$emit('error');
+ },
+ },
+ gitpodEnabled: {
+ query: applicationInfoQuery,
+ error() {
+ this.$emit('error');
+ },
+ },
},
props: {
blob: {
@@ -52,10 +70,26 @@ export default {
required: false,
default: false,
},
+ showForkSuggestion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ projectId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
viewer: this.hideViewerSwitcher ? null : this.activeViewerType,
+ gitpodEnabled: false,
};
},
computed: {
@@ -65,12 +99,18 @@ export default {
showDefaultActions() {
return !this.hideDefaultActions;
},
+ showWebIdeLink() {
+ return !this.blob.archived && this.blob.editBlobPath;
+ },
isEmpty() {
return this.blob.rawSize === '0';
},
blobSwitcherDocIcon() {
return this.blob.richViewer?.fileType === 'csv' ? 'table' : 'document';
},
+ projectIdAsNumber() {
+ return getIdFromGraphQLId(this.projectId);
+ },
},
watch: {
viewer(newVal, oldVal) {
@@ -100,6 +140,27 @@ export default {
<div class="gl-display-flex gl-flex-wrap file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" :doc-icon="blobSwitcherDocIcon" />
+ <web-ide-link
+ v-if="showWebIdeLink"
+ :show-edit-button="!isBinary"
+ class="gl-mr-3"
+ :edit-url="blob.editBlobPath"
+ :web-ide-url="blob.ideEditPath"
+ :needs-to-fork="showForkSuggestion"
+ :show-pipeline-editor-button="Boolean(blob.pipelineEditorPath)"
+ :pipeline-editor-url="blob.pipelineEditorPath"
+ :gitpod-url="blob.gitpodBlobUrl"
+ :show-gitpod-button="gitpodEnabled"
+ :gitpod-enabled="currentUser && currentUser.gitpodEnabled"
+ :project-path="projectPath"
+ :project-id="projectIdAsNumber"
+ :user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
+ :user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
+ is-blob
+ disable-fork-modal
+ v-on="$listeners"
+ />
+
<slot name="actions"></slot>
<default-actions
diff --git a/app/assets/javascripts/blob/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js
index 4258d16b69f..9c6a5958e1f 100644
--- a/app/assets/javascripts/blob/line_highlighter.js
+++ b/app/assets/javascripts/blob/line_highlighter.js
@@ -59,7 +59,7 @@ LineHighlighter.prototype.bindEvents = function () {
}
};
-LineHighlighter.prototype.highlightHash = function (newHash) {
+LineHighlighter.prototype.highlightHash = function (newHash, scrollEnabled = true) {
let range;
if (newHash && typeof newHash === 'string') this._hash = newHash;
@@ -71,12 +71,14 @@ LineHighlighter.prototype.highlightHash = function (newHash) {
this.highlightRange(range);
const lineSelector = `#L${range[0]}`;
- scrollToElement(lineSelector, {
- // Scroll to the first highlighted line on initial load
- // Add an offset of -100 for some context
- offset: -100,
- behavior: this.options.scrollBehavior,
- });
+ if (scrollEnabled) {
+ scrollToElement(lineSelector, {
+ // Scroll to the first highlighted line on initial load
+ // Add an offset of -100 for some context
+ offset: -100,
+ behavior: this.options.scrollBehavior,
+ });
+ }
}
}
};
@@ -94,7 +96,8 @@ LineHighlighter.prototype.clickHandler = function (event) {
// treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
- } else if (event.shiftKey) {
+ }
+ if (event.shiftKey) {
if (lineNumber < current[0]) {
range = [lineNumber, current[0]];
} else {
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 94ae281cada..9c22d960bf5 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -5,6 +5,7 @@ import {
relativePathToAbsolute,
joinPaths,
setUrlParams,
+ getParameterByName,
} from '~/lib/utils/url_utility';
const SANDBOX_FRAME_PATH = '/-/sandbox/swagger';
@@ -12,10 +13,14 @@ const SANDBOX_FRAME_PATH = '/-/sandbox/swagger';
const getSandboxFrameSrc = () => {
const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH);
const absoluteUrl = relativePathToAbsolute(path, getBaseURL());
+ const displayOperationId = getParameterByName('displayOperationId');
+ const params = { displayOperationId };
+
if (window.gon?.relative_url_root) {
- return setUrlParams({ relativeRootPath: window.gon.relative_url_root }, absoluteUrl);
+ params.relativeRootPath = window.gon.relative_url_root;
}
- return absoluteUrl;
+
+ return setUrlParams(params, absoluteUrl);
};
const createSandbox = () => {
diff --git a/app/assets/javascripts/repository/queries/application_info.query.graphql b/app/assets/javascripts/blob/queries/application_info.query.graphql
index fd69de39f75..fd69de39f75 100644
--- a/app/assets/javascripts/repository/queries/application_info.query.graphql
+++ b/app/assets/javascripts/blob/queries/application_info.query.graphql
diff --git a/app/assets/javascripts/repository/queries/user_info.query.graphql b/app/assets/javascripts/blob/queries/user_info.query.graphql
index 114947a423d..114947a423d 100644
--- a/app/assets/javascripts/repository/queries/user_info.query.graphql
+++ b/app/assets/javascripts/blob/queries/user_info.query.graphql
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 692ca6bf59b..c441a718dd8 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -20,6 +20,7 @@ import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';
+import { setError } from '../graphql/cache_updates';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
@@ -45,6 +46,7 @@ export default {
},
mixins: [boardCardInner],
inject: [
+ 'allowSubEpics',
'rootPath',
'scopedLabelsAvailable',
'isEpicBoard',
@@ -85,7 +87,7 @@ export default {
};
},
computed: {
- ...mapState(['isShowingLabels', 'allowSubEpics']),
+ ...mapState(['isShowingLabels']),
isLoading() {
return this.item.isLoading || this.item.iid === '-1';
},
@@ -175,7 +177,8 @@ export default {
},
},
methods: {
- ...mapActions(['performSearch', 'setError']),
+ ...mapActions(['performSearch']),
+ setError,
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
@@ -288,7 +291,7 @@ export default {
<gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-5" />
<span
v-if="item.referencePath && !isLoading"
- class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3 gl-font-sm gl-text-secondary"
+ class="board-card-number gl-overflow-hidden gl-display-flex gl-gap-2 gl-mr-3 gl-mt-3 gl-font-sm gl-text-secondary"
:class="{ 'gl-font-base': isEpicBoard }"
>
<work-item-type-icon
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 5e1e46dd198..bb740c0e7eb 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -222,7 +222,7 @@ export default {
<template #default>
<board-sidebar-title :active-item="activeBoardIssuable" data-testid="sidebar-title" />
<sidebar-assignees-widget
- v-if="activeBoardItem.assignees"
+ v-if="activeBoardIssuable.assignees"
:iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:initial-assignees="activeBoardIssuable.assignees"
@@ -232,7 +232,7 @@ export default {
/>
<sidebar-dropdown-widget
v-if="epicFeatureAvailable && !isIncidentSidebar"
- :key="`epic-${activeBoardItem.iid}`"
+ :key="`epic-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
@@ -242,7 +242,7 @@ export default {
/>
<div>
<sidebar-dropdown-widget
- :key="`milestone-${activeBoardItem.iid}`"
+ :key="`milestone-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
@@ -252,7 +252,7 @@ export default {
/>
<sidebar-iteration-widget
v-if="iterationFeatureAvailable && !isIncidentSidebar"
- :key="`iteration-${activeBoardItem.iid}`"
+ :key="`iteration-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 4986c3780e5..d12478b42d8 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -178,6 +178,9 @@ export default {
},
methods: {
...mapActions(['setError', 'unsetError', 'setBoard']),
+ isFocusMode() {
+ return Boolean(document.querySelector('.content-wrapper > .js-focus-mode-board.is-focused'));
+ },
cancel() {
this.$emit('cancel');
},
@@ -281,6 +284,7 @@ export default {
modal-class="board-config-modal"
content-class="gl-absolute gl-top-7"
visible
+ :static="isFocusMode()"
:hide-footer="readonly"
:title="title"
:action-primary="primaryProps"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 67bfcfb9d97..1bb7e88122a 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -88,6 +88,7 @@ export default {
toListId: null,
toList: {},
addItemToListInProgress: false,
+ updateIssueOrderInProgress: false,
};
},
apollo: {
@@ -253,7 +254,9 @@ export default {
return this.canMoveIssue ? options : {};
},
disableScrollingWhenMutationInProgress() {
- return this.hasNextPage && this.isUpdateIssueOrderInProgress;
+ return (
+ this.hasNextPage && (this.isUpdateIssueOrderInProgress || this.updateIssueOrderInProgress)
+ );
},
showMoveToPosition() {
return !this.disabled && this.list.listType !== ListType.closed;
@@ -343,7 +346,7 @@ export default {
sortableStart();
this.track('drag_card', { label: 'board' });
},
- handleDragOnEnd({
+ async handleDragOnEnd({
newIndex: originalNewIndex,
oldIndex,
from,
@@ -394,7 +397,8 @@ export default {
}
if (this.isApolloBoard) {
- this.moveBoardItem(
+ this.updateIssueOrderInProgress = true;
+ await this.moveBoardItem(
{
epicId: itemId,
iid: itemIid,
@@ -404,7 +408,9 @@ export default {
moveAfterId,
},
newIndex,
- );
+ ).finally(() => {
+ this.updateIssueOrderInProgress = false;
+ });
} else {
this.moveItem({
itemId,
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 068db98a750..42c30dc8245 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -490,7 +490,11 @@ export default {
>
<span class="gl-display-inline-flex" :class="{ 'gl-rotate-90': list.collapsed }">
<gl-tooltip :target="() => $refs.itemCount" :title="itemsTooltipLabel" />
- <span ref="itemCount" class="gl-display-inline-flex gl-align-items-center">
+ <span
+ ref="itemCount"
+ class="gl-display-inline-flex gl-align-items-center"
+ data-testid="item-count"
+ >
<gl-icon class="gl-mr-2" :name="countIcon" :size="14" />
<item-count
v-if="!isLoading"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 58db2c9ac2a..89e13625210 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -166,7 +166,7 @@ export default {
<mounting-portal mount-to="#js-right-sidebar-portal" name="board-settings-sidebar" append>
<gl-drawer
v-bind="$attrs"
- class="js-board-settings-sidebar gl-absolute boards-sidebar"
+ class="js-board-settings-sidebar boards-sidebar"
:open="showSidebar"
variant="sidebar"
@close="unsetActiveListId"
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 2b8418333a8..7fd1a934381 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -93,6 +93,9 @@ export default {
});
return hasScope;
},
+ isLoading() {
+ return this.$apollo.queries.board.loading;
+ },
},
};
</script>
@@ -105,7 +108,11 @@ export default {
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0 gl-mb-3 gl-w-full gl-min-w-0"
>
- <boards-selector :board-apollo="board" @switchBoard="$emit('switchBoard', $event)" />
+ <boards-selector
+ :board-apollo="board"
+ :is-current-board-loading="isLoading"
+ @switchBoard="$emit('switchBoard', $event)"
+ />
<new-board-button />
<issue-board-filtered-search
v-if="isIssueBoard"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index b3fe52944dc..cc6fde92f9b 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -70,6 +70,11 @@ export default {
required: false,
default: () => ({}),
},
+ isCurrentBoardLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -92,6 +97,9 @@ export default {
boardToUse() {
return this.isApolloBoard ? this.boardApollo : this.board;
},
+ isBoardToUseLoading() {
+ return this.isApolloBoard ? this.isCurrentBoardLoading : this.isBoardLoading;
+ },
parentType() {
return this.boardType;
},
@@ -301,7 +309,7 @@ export default {
data-qa-selector="boards_dropdown"
toggle-class="dropdown-menu-toggle"
menu-class="flex-column dropdown-extended-height"
- :loading="isBoardLoading"
+ :loading="isBoardToUseLoading"
:text="boardToUse.name"
@show="loadBoards"
>
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index f60f00be368..a7b3f5536a4 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -62,7 +62,7 @@ export default {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchUsers, fetchLabels, fetchMilestones } = issueBoardFilters(
+ const { fetchUsers, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
this.isGroupBoard,
@@ -148,7 +148,8 @@ export default {
token: MilestoneToken,
unique: true,
shouldSkipSort: true,
- fetchMilestones,
+ isProject: !this.isGroupBoard,
+ fullPath: this.fullPath,
},
{
icon: 'issues',
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
index 1f28974afd1..46009df2bd3 100644
--- a/app/assets/javascripts/boards/components/issue_due_date.vue
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -52,11 +52,14 @@ export default {
if (timeDifference === 0) {
return __('Today');
- } else if (timeDifference === 1) {
+ }
+ if (timeDifference === 1) {
return __('Tomorrow');
- } else if (timeDifference === -1) {
+ }
+ if (timeDifference === -1) {
return __('Yesterday');
- } else if (timeDifference > 0 && timeDifference < 7) {
+ }
+ if (timeDifference > 0 && timeDifference < 7) {
return dateFormat(issueDueDate, 'dddd');
}
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 1c2c0022ddf..a2c4b42b6c5 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -7,6 +7,7 @@ import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { titleQueries } from 'ee_else_ce/boards/constants';
+import { setError } from '../../graphql/cache_updates';
export default {
components: {
@@ -65,7 +66,7 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveItemTitle', 'setError']),
+ ...mapActions(['setActiveItemTitle']),
getPendingChangesKey(item) {
if (!item) {
return '';
@@ -130,7 +131,7 @@ export default {
this.showChangesAlert = false;
} catch (e) {
this.title = this.item.title;
- this.setError({ error: e, message: this.$options.i18n.updateTitleError });
+ setError({ error: e, message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/boards/graphql/cache_updates.js b/app/assets/javascripts/boards/graphql/cache_updates.js
index e54701a63c0..3551c3ed982 100644
--- a/app/assets/javascripts/boards/graphql/cache_updates.js
+++ b/app/assets/javascripts/boards/graphql/cache_updates.js
@@ -109,9 +109,9 @@ export function updateEpicsCount({
epicBoardList: {
...epicBoardList,
metadata: {
+ ...epicBoardList.metadata,
epicsCount: epicBoardList.metadata.epicsCount - 1,
totalWeight: epicBoardList.metadata.totalWeight - epicWeight,
- ...epicBoardList.metadata,
},
},
}),
@@ -127,9 +127,9 @@ export function updateEpicsCount({
epicBoardList: {
...epicBoardList,
metadata: {
+ ...epicBoardList.metadata,
epicsCount: epicBoardList.metadata.epicsCount + 1,
totalWeight: epicBoardList.metadata.totalWeight + epicWeight,
- ...epicBoardList.metadata,
},
},
}),
diff --git a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql b/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
deleted file mode 100644
index 252e8c1ab06..00000000000
--- a/app/assets/javascripts/boards/graphql/group_board_members.query.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
-
-query GroupBoardMembers($fullPath: ID!, $search: String) {
- workspace: group(fullPath: $fullPath) {
- id
- assignees: groupMembers(search: $search, relations: [DIRECT, DESCENDANTS, INHERITED]) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql b/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
deleted file mode 100644
index 5279680b03c..00000000000
--- a/app/assets/javascripts/boards/graphql/project_board_members.query.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
-
-query ProjectBoardMembers($fullPath: ID!, $search: String) {
- workspace: project(fullPath: $fullPath) {
- id
- assignees: projectMembers(search: $search) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a03ec9193ea..9d7b7a38c6d 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -102,6 +102,7 @@ function mountBoardApp(el) {
swimlanesFeatureAvailable: gon.licensed_features?.swimlanes,
multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleBoardsAvailable),
scopedIssueBoardFeatureEnabled: parseBoolean(el.dataset.scopedIssueBoardFeatureEnabled),
+ allowSubEpics: false,
},
render: (createComponent) => createComponent(BoardApp),
});
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index 27efb3f775c..ba5da70c6ec 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -1,7 +1,5 @@
-import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql';
-import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql';
-import groupBoardMilestonesQuery from './graphql/group_board_milestones.query.graphql';
-import projectBoardMilestonesQuery from './graphql/project_board_milestones.query.graphql';
+import { BoardType } from 'ee_else_ce/boards/constants';
+import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import boardLabels from './graphql/board_labels.query.graphql';
export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
@@ -9,20 +7,15 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
};
- const boardAssigneesQuery = () => {
- return isGroupBoard ? groupBoardMembers : projectBoardMembers;
- };
-
const fetchUsers = (usersSearchTerm) => {
+ const namespace = isGroupBoard ? BoardType.group : BoardType.project;
+
return apollo
.query({
- query: boardAssigneesQuery(),
- variables: {
- fullPath,
- search: usersSearchTerm,
- },
+ query: usersAutocompleteQuery,
+ variables: { fullPath, search: usersSearchTerm, isProject: !isGroupBoard },
})
- .then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user));
+ .then(({ data }) => data[namespace]?.autocompleteUsers);
};
const fetchLabels = (labelSearchTerm) => {
@@ -39,27 +32,8 @@ export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
.then(transformLabels);
};
- const fetchMilestones = (searchTerm) => {
- const variables = {
- fullPath,
- searchTerm,
- };
-
- const query = isGroupBoard ? groupBoardMilestonesQuery : projectBoardMilestonesQuery;
-
- return apollo
- .query({
- query,
- variables,
- })
- .then(({ data }) => {
- return data.workspace?.milestones.nodes;
- });
- };
-
return {
fetchLabels,
fetchUsers,
- fetchMilestones,
};
}
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
index daa4119f44d..89582e64f3a 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/admin_jobs_table_app.vue
@@ -1,13 +1,17 @@
<script>
import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
-import { validateQueryString } from '~/jobs/components/filtered_search/utils';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import JobsTableTabs from '~/jobs/components/table/jobs_table_tabs.vue';
-import JobsFilteredSearch from '~/jobs/components/filtered_search/jobs_filtered_search.vue';
-import JobsTableEmptyState from '~/jobs/components/table/jobs_table_empty_state.vue';
+import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import JobsTableTabs from '~/ci/jobs_page/components/jobs_table_tabs.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import JobsTableEmptyState from '~/ci/jobs_page/components/jobs_table_empty_state.vue';
import { createAlert } from '~/alert';
-import JobsSkeletonLoader from '../jobs_skeleton_loader.vue';
+import {
+ TOKEN_TYPE_STATUS,
+ TOKEN_TYPE_JOBS_RUNNER_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
DEFAULT_FIELDS_ADMIN,
RAW_TEXT_WARNING_ADMIN,
@@ -15,10 +19,11 @@ import {
JOBS_FETCH_ERROR_MSG,
LOADING_ARIA_LABEL,
CANCELABLE_JOBS_ERROR_MSG,
-} from '../constants';
+} from './constants';
+import JobsSkeletonLoader from './components/jobs_skeleton_loader.vue';
import GetAllJobs from './graphql/queries/get_all_jobs.query.graphql';
import GetAllJobsCount from './graphql/queries/get_all_jobs_count.query.graphql';
-import CancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
+import getCancelableJobs from './graphql/queries/get_cancelable_jobs_count.query.graphql';
export default {
i18n: {
@@ -39,6 +44,7 @@ export default {
GlIntersectionObserver,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
jobStatuses: {
default: null,
@@ -72,6 +78,9 @@ export default {
},
jobsCount: {
query: GetAllJobsCount,
+ variables() {
+ return this.variables;
+ },
update(data) {
return data?.jobs?.count || 0;
},
@@ -83,7 +92,7 @@ export default {
},
},
cancelable: {
- query: CancelableJobs,
+ query: getCancelableJobs,
update(data) {
this.isCancelable = data.cancelable.count !== 0;
},
@@ -150,11 +159,11 @@ export default {
},
},
methods: {
- updateHistoryAndFetchCount(status = null) {
- this.$apollo.queries.jobsCount.refetch({ statuses: status });
+ updateHistoryAndFetchCount(filterParams = {}) {
+ this.$apollo.queries.jobsCount.refetch(filterParams);
updateHistory({
- url: setUrlParams({ statuses: status }, window.location.href, true),
+ url: setUrlParams(filterParams, window.location.href, true),
});
},
fetchJobsByStatus(scope) {
@@ -184,33 +193,36 @@ export default {
this.infiniteScrollingTriggered = false;
this.filterSearchTriggered = true;
- // all filters have been cleared reset query param
- // and refetch jobs/count with defaults
- if (!filters.length) {
- this.updateHistoryAndFetchCount();
- this.$apollo.queries.jobs.refetch({ statuses: null });
-
- return;
- }
-
- // Eventually there will be more tokens available
- // this code is written to scale for those tokens
- filters.forEach((filter) => {
+ if (filters.some((filter) => !filter.type)) {
// Raw text input in filtered search does not have a type
// when a user enters raw text we alert them that it is
// not supported and we do not make an additional API call
- if (!filter.type) {
- createAlert({
- message: RAW_TEXT_WARNING_ADMIN,
- type: 'warning',
- });
- }
+ createAlert({ message: RAW_TEXT_WARNING_ADMIN, type: 'warning' });
+ return;
+ }
+
+ const defaultFilterParams = this.glFeatures.adminJobsFilterRunnerType
+ ? { statuses: null, runnerTypes: null }
+ : { statuses: null };
- if (filter.type === 'status') {
- this.updateHistoryAndFetchCount(filter.value.data);
- this.$apollo.queries.jobs.refetch({ statuses: filter.value.data });
+ const filterParams = filters.reduce((acc, filter) => {
+ switch (filter.type) {
+ case TOKEN_TYPE_STATUS:
+ return { ...acc, statuses: filter.value.data };
+
+ case TOKEN_TYPE_JOBS_RUNNER_TYPE:
+ if (this.glFeatures.adminJobsFilterRunnerType) {
+ return { ...acc, runnerTypes: filter.value.data };
+ }
+ return acc;
+
+ default:
+ return acc;
}
- });
+ }, defaultFilterParams);
+
+ this.updateHistoryAndFetchCount(filterParams);
+ this.$apollo.queries.jobs.refetch(filterParams);
},
},
};
diff --git a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue
index 72cfc005782..fb13fd4b03e 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { CANCEL_JOBS_MODAL_ID, CANCEL_JOBS_BUTTON_TEXT, CANCEL_BUTTON_TOOLTIP } from '../constants';
import CancelJobsModal from './cancel_jobs_modal.vue';
-import { CANCEL_JOBS_MODAL_ID, CANCEL_JOBS_BUTTON_TEXT, CANCEL_BUTTON_TOOLTIP } from './constants';
export default {
name: 'CancelJobs',
diff --git a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs_modal.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue
index b2c5326fefd..7c1cd75609a 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/cancel_jobs_modal.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cancel_jobs_modal.vue
@@ -9,7 +9,7 @@ import {
CANCEL_JOBS_MODAL_TITLE,
CANCEL_JOBS_WARNING,
PRIMARY_ACTION_TEXT,
-} from './constants';
+} from '../constants';
export default {
components: {
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
index cbb80a5175f..cbb80a5175f 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/project_cell.vue
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
index 33bcee5b34b..a76829aa129 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/cells/runner_cell.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/cells/runner_cell.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '~/pages/admin/jobs/components/constants';
+import { RUNNER_EMPTY_TEXT, RUNNER_NO_DESCRIPTION } from '../../constants';
export default {
i18n: {
diff --git a/app/assets/javascripts/pages/admin/jobs/components/jobs_skeleton_loader.vue b/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue
index c305e09af0d..c305e09af0d 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/jobs_skeleton_loader.vue
+++ b/app/assets/javascripts/ci/admin/jobs_table/components/jobs_skeleton_loader.vue
diff --git a/app/assets/javascripts/pages/admin/jobs/components/constants.js b/app/assets/javascripts/ci/admin/jobs_table/constants.js
index 4af8cb355fc..ff0efdb1f5b 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/constants.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/constants.js
@@ -1,5 +1,5 @@
import { s__, __ } from '~/locale';
-import { RAW_TEXT_WARNING } from '~/jobs/components/table/constants';
+import { RAW_TEXT_WARNING } from '~/ci/jobs_page/constants';
export const JOBS_COUNT_ERROR_MESSAGE = __('There was an error fetching the number of jobs.');
export const JOBS_FETCH_ERROR_MSG = __('There was an error fetching the jobs.');
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js
index fd7ee2a6f8c..fd7ee2a6f8c 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/cache_config.js
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/cache_config.js
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
index 9e2795966e0..89fb1782e46 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs.query.graphql
@@ -1,5 +1,10 @@
-query getAllJobs($after: String, $first: Int = 50, $statuses: [CiJobStatus!]) {
- jobs(after: $after, first: $first, statuses: $statuses) {
+query getAllJobs(
+ $after: String
+ $first: Int = 50
+ $statuses: [CiJobStatus!]
+ $runnerTypes: [CiRunnerType!]
+) {
+ jobs(after: $after, first: $first, statuses: $statuses, runnerTypes: $runnerTypes) {
pageInfo {
endCursor
hasNextPage
diff --git a/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql
new file mode 100644
index 00000000000..bcb0123e9e3
--- /dev/null
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_all_jobs_count.query.graphql
@@ -0,0 +1,5 @@
+query getAllJobsCount($statuses: [CiJobStatus!], $runnerTypes: [CiRunnerType!]) {
+ jobs(statuses: $statuses, runnerTypes: $runnerTypes) {
+ count
+ }
+}
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql
index 9b90abebbf7..9bf5e1449b7 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_cancelable_jobs_count.query.graphql
+++ b/app/assets/javascripts/ci/admin/jobs_table/graphql/queries/get_cancelable_jobs_count.query.graphql
@@ -1,4 +1,4 @@
-query canelableJobs {
+query getCancelableJobsCount {
cancelable: jobs(statuses: [PENDING, RUNNING]) {
count
}
diff --git a/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue b/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue
deleted file mode 100644
index d2c96b1a201..00000000000
--- a/app/assets/javascripts/ci/artifacts/components/feedback_banner.vue
+++ /dev/null
@@ -1,41 +0,0 @@
-<script>
-import { GlBanner } from '@gitlab/ui';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-import {
- I18N_FEEDBACK_BANNER_TITLE,
- I18N_FEEDBACK_BANNER_BODY,
- I18N_FEEDBACK_BANNER_BUTTON,
- FEEDBACK_URL,
-} from '../constants';
-
-export default {
- components: {
- GlBanner,
- UserCalloutDismisser,
- },
- inject: ['artifactsManagementFeedbackImagePath'],
- FEEDBACK_URL,
- i18n: {
- title: I18N_FEEDBACK_BANNER_TITLE,
- body: I18N_FEEDBACK_BANNER_BODY,
- button: I18N_FEEDBACK_BANNER_BUTTON,
- },
-};
-</script>
-<template>
- <user-callout-dismisser feature-name="artifacts_management_page_feedback_banner">
- <template #default="{ dismiss, shouldShowCallout }">
- <gl-banner
- v-if="shouldShowCallout"
- class="gl-mb-6"
- :title="$options.i18n.title"
- :button-text="$options.i18n.button"
- :button-link="$options.FEEDBACK_URL"
- :svg-path="artifactsManagementFeedbackImagePath"
- @close="dismiss"
- >
- <p>{{ $options.i18n.body }}</p>
- </gl-banner>
- </template>
- </user-callout-dismisser>
-</template>
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 88334488fdd..e08470c62be 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -48,7 +48,6 @@ import JobCheckbox from './job_checkbox.vue';
import ArtifactsBulkDelete from './artifacts_bulk_delete.vue';
import BulkDeleteModal from './bulk_delete_modal.vue';
import ArtifactsTableRowDetails from './artifacts_table_row_details.vue';
-import FeedbackBanner from './feedback_banner.vue';
const INITIAL_PAGINATION_STATE = {
currentPage: INITIAL_CURRENT_PAGE,
@@ -76,7 +75,6 @@ export default {
ArtifactsBulkDelete,
BulkDeleteModal,
ArtifactsTableRowDetails,
- FeedbackBanner,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -374,7 +372,6 @@ export default {
</script>
<template>
<div>
- <feedback-banner />
<artifacts-bulk-delete
v-if="canBulkDestroyArtifacts"
:selected-artifacts="selectedArtifacts"
diff --git a/app/assets/javascripts/ci/artifacts/constants.js b/app/assets/javascripts/ci/artifacts/constants.js
index 2d89b6541f3..28c371cda1e 100644
--- a/app/assets/javascripts/ci/artifacts/constants.js
+++ b/app/assets/javascripts/ci/artifacts/constants.js
@@ -47,13 +47,6 @@ export const I18N_MODAL_BODY = s__(
export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
export const I18N_MODAL_CANCEL = __('Cancel');
-export const I18N_FEEDBACK_BANNER_TITLE = s__('Artifacts|Help us improve this page');
-export const I18N_FEEDBACK_BANNER_BODY = s__(
- 'Artifacts|We want you to be able to use this page to easily manage your CI/CD job artifacts. We are working to improve this experience and would appreciate any feedback you have about the improvements we are making.',
-);
-export const I18N_FEEDBACK_BANNER_BUTTON = s__('Artifacts|Take a quick survey');
-export const FEEDBACK_URL = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_cI9rAUI20Vo2St8';
-
export const SELECTED_ARTIFACTS_MAX_COUNT = 50;
export const I18N_BULK_DELETE_MAX_SELECTED = s__(
'Artifacts|Maximum selected artifacts limit reached',
diff --git a/app/assets/javascripts/ci/artifacts/index.js b/app/assets/javascripts/ci/artifacts/index.js
index 6e795fd9bd7..c6021eb056f 100644
--- a/app/assets/javascripts/ci/artifacts/index.js
+++ b/app/assets/javascripts/ci/artifacts/index.js
@@ -19,12 +19,7 @@ export const initArtifactsTable = () => {
return false;
}
- const {
- projectPath,
- projectId,
- canDestroyArtifacts,
- artifactsManagementFeedbackImagePath,
- } = el.dataset;
+ const { projectPath, projectId, canDestroyArtifacts } = el.dataset;
return new Vue({
el,
@@ -33,7 +28,6 @@ export const initArtifactsTable = () => {
projectPath,
projectId,
canDestroyArtifacts: parseBoolean(canDestroyArtifacts),
- artifactsManagementFeedbackImagePath,
},
render: (createElement) => createElement(App),
});
diff --git a/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js
deleted file mode 100644
index 574a5e7fd99..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js
+++ /dev/null
@@ -1,262 +0,0 @@
-import $ from 'jquery';
-import SecretValues from '~/behaviors/secret_values';
-import CreateItemDropdown from '~/create_item_dropdown';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import { s__ } from '~/locale';
-
-const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments');
-
-function createEnvironmentItem(value) {
- return {
- title: value === '*' ? ALL_ENVIRONMENTS_STRING : value,
- id: value,
- text: value === '*' ? s__('CiVariable|* (All environments)') : value,
- };
-}
-
-export default class VariableList {
- constructor({ container, formField, maskableRegex }) {
- this.$container = $(container);
- this.formField = formField;
- this.maskableRegex = new RegExp(maskableRegex);
- this.environmentDropdownMap = new WeakMap();
-
- this.inputMap = {
- id: {
- selector: '.js-ci-variable-input-id',
- default: '',
- },
- variable_type: {
- selector: '.js-ci-variable-input-variable-type',
- default: 'env_var',
- },
- key: {
- selector: '.js-ci-variable-input-key',
- default: '',
- },
- secret_value: {
- selector: '.js-ci-variable-input-value',
- default: '',
- },
- protected: {
- selector: '.js-ci-variable-input-protected',
- // use `attr` instead of `data` as we don't want the value to be
- // converted. we need the value as a string.
- default: $('.js-ci-variable-input-protected').attr('data-default'),
- },
- masked: {
- selector: '.js-ci-variable-input-masked',
- // use `attr` instead of `data` as we don't want the value to be
- // converted. we need the value as a string.
- default: $('.js-ci-variable-input-masked').attr('data-default'),
- },
- environment_scope: {
- // We can't use a `.js-` class here because
- // deprecated_jquery_dropdown replaces the <input> and doesn't copy over the class
- // See https://gitlab.com/gitlab-org/gitlab-foss/issues/42458
- selector: `input[name="${this.formField}[variables_attributes][][environment_scope]"]`,
- default: '*',
- },
- _destroy: {
- selector: '.js-ci-variable-input-destroy',
- default: '',
- },
- };
-
- this.secretValues = new SecretValues({
- container: this.$container[0],
- valueSelector: '.js-row:not(:last-child) .js-secret-value',
- placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder',
- });
- }
-
- init() {
- this.bindEvents();
- this.secretValues.init();
- }
-
- bindEvents() {
- this.$container.find('.js-row').each((index, rowEl) => {
- this.initRow(rowEl);
- });
-
- this.$container.on('click', '.js-row-remove-button', (e) => {
- e.preventDefault();
- this.removeRow($(e.currentTarget).closest('.js-row'));
- });
-
- const inputSelector = Object.keys(this.inputMap)
- .map((name) => this.inputMap[name].selector)
- .join(',');
-
- // Remove any empty rows except the last row
- this.$container.on('blur', inputSelector, (e) => {
- const $row = $(e.currentTarget).closest('.js-row');
-
- if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
- this.removeRow($row);
- }
- });
-
- this.$container.on('input trigger-change', inputSelector, (e) => {
- // Always make sure there is an empty last row
- const $lastRow = this.$container.find('.js-row').last();
-
- if (this.checkIfRowTouched($lastRow)) {
- this.insertRow($lastRow);
- }
-
- // If masked, validate value against regex
- this.validateMaskability($(e.currentTarget).closest('.js-row'));
- });
- }
-
- initRow(rowEl) {
- const $row = $(rowEl);
-
- // Reset the resizable textarea
- $row.find(this.inputMap.secret_value.selector).css('height', '');
-
- const $environmentSelect = $row.find('.js-variable-environment-toggle');
- if ($environmentSelect.length) {
- const createItemDropdown = new CreateItemDropdown({
- $dropdown: $environmentSelect,
- defaultToggleLabel: ALL_ENVIRONMENTS_STRING,
- fieldName: `${this.formField}[variables_attributes][][environment_scope]`,
- getData: (term, callback) => callback(this.getEnvironmentValues()),
- createNewItemFromValue: createEnvironmentItem,
- onSelect: () => {
- // Refresh the other dropdowns in the variable list
- // so they have the new value we just picked
- this.refreshDropdownData();
-
- $row.find(this.inputMap.environment_scope.selector).trigger('trigger-change');
- },
- });
-
- // Clear out any data that might have been left-over from the row clone
- createItemDropdown.clearDropdown();
-
- this.environmentDropdownMap.set($row[0], createItemDropdown);
- }
- }
-
- insertRow($row) {
- const $rowClone = $row.clone();
- $rowClone.removeAttr('data-is-persisted');
-
- // Reset the inputs to their defaults
- Object.keys(this.inputMap).forEach((name) => {
- const entry = this.inputMap[name];
- $rowClone.find(entry.selector).val(entry.default);
- });
-
- // Close any dropdowns
- $rowClone.find('.dropdown-menu.show').each((index, $dropdown) => {
- $dropdown.classList.remove('show');
- });
-
- this.initRow($rowClone);
-
- $row.after($rowClone);
- }
-
- removeRow(row) {
- const $row = $(row);
- const isPersisted = parseBoolean($row.attr('data-is-persisted'));
-
- if (isPersisted) {
- $row.hide();
- $row
- // eslint-disable-next-line no-underscore-dangle
- .find(this.inputMap._destroy.selector)
- .val(true);
- } else {
- $row.remove();
- }
-
- // Refresh the other dropdowns in the variable list
- // so any value with the variable deleted is gone
- this.refreshDropdownData();
- }
-
- checkIfRowTouched($row) {
- return Object.keys(this.inputMap).some((name) => {
- // Row should not qualify as touched if only switches have been touched
- if (['protected', 'masked'].includes(name)) return false;
-
- const entry = this.inputMap[name];
- const $el = $row.find(entry.selector);
- return $el.length && $el.val() !== entry.default;
- });
- }
-
- validateMaskability($row) {
- const invalidInputClass = 'gl-field-error-outline';
-
- const variableValue = $row.find(this.inputMap.secret_value.selector).val();
- const isValueMaskable = this.maskableRegex.test(variableValue) || variableValue === '';
- const isMaskedChecked = $row.find(this.inputMap.masked.selector).val() === 'true';
-
- // Show a validation error if the user wants to mask an unmaskable variable value
- $row
- .find(this.inputMap.secret_value.selector)
- .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
- $row
- .find('.js-secret-value-placeholder')
- .toggleClass(invalidInputClass, isMaskedChecked && !isValueMaskable);
- $row.find('.masking-validation-error').toggle(isMaskedChecked && !isValueMaskable);
- }
-
- toggleEnableRow(isEnabled = true) {
- this.$container.find(this.inputMap.key.selector).attr('disabled', !isEnabled);
- this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
- }
-
- hideValues() {
- this.secretValues.updateDom(false);
- }
-
- getAllData() {
- // Ignore the last empty row because we don't want to try persist
- // a blank variable and run into validation problems.
- const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
-
- return validRows.map((rowEl) => {
- const resultant = {};
- Object.keys(this.inputMap).forEach((name) => {
- const entry = this.inputMap[name];
- const $input = $(rowEl).find(entry.selector);
- if ($input.length) {
- resultant[name] = $input.val();
- }
- });
-
- return resultant;
- });
- }
-
- getEnvironmentValues() {
- const valueMap = this.$container
- .find(this.inputMap.environment_scope.selector)
- .toArray()
- .reduce(
- (prevValueMap, envInput) => ({
- ...prevValueMap,
- [envInput.value]: envInput.value,
- }),
- {},
- );
-
- return Object.keys(valueMap).map(createEnvironmentItem);
- }
-
- refreshDropdownData() {
- this.$container.find('.js-row').each((index, rowEl) => {
- const environmentDropdown = this.environmentDropdownMap.get(rowEl);
- if (environmentDropdown) {
- environmentDropdown.refreshData();
- }
- });
- }
-}
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 0ce11da658c..c609e05bbb7 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
@@ -1,10 +1,12 @@
<script>
import {
+ GlAlert,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
+ GlFormInput,
GlFormSelect,
GlFormTextarea,
GlIcon,
@@ -15,9 +17,14 @@ import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
+import Tracking from '~/tracking';
import {
+ allEnvironments,
defaultVariableState,
+ DRAWER_EVENT_LABEL,
+ EDIT_VARIABLE_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
+ EVENT_ACTION,
EXPANDED_VARIABLES_NOTE,
FLAG_LINK_TITLE,
VARIABLE_ACTIONS,
@@ -26,9 +33,13 @@ import {
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokenList } from './ci_variable_autocomplete_tokens';
-const i18n = {
+const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
+
+export const i18n = {
addVariable: s__('CiVariables|Add Variable'),
cancel: __('Cancel'),
+ defaultScope: allEnvironments.text,
+ editVariable: s__('CiVariables|Edit Variable'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
@@ -44,39 +55,60 @@ const i18n = {
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
),
+ valueFeedback: {
+ rawHelpText: s__('CiVariables|Variable value will be evaluated as raw string.'),
+ maskedReqsNotMet: s__(
+ 'CiVariables|This variable value does not meet the masking requirements.',
+ ),
+ },
+ variableReferenceTitle: s__('CiVariables|Value might contain a variable reference'),
+ variableReferenceDescription: s__(
+ 'CiVariables|Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
+ ),
type: __('Type'),
value: __('Value'),
};
+const VARIABLE_REFERENCE_REGEX = /\$/;
+
export default {
DRAWER_Z_INDEX,
components: {
CiEnvironmentsDropdown,
+ GlAlert,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
+ GlFormInput,
GlFormSelect,
GlFormTextarea,
GlIcon,
GlLink,
GlSprintf,
},
- inject: ['environmentScopeLink'],
+ mixins: [trackingMixin],
+ inject: ['environmentScopeLink', 'isProtectedByDefault', 'maskableRawRegex', 'maskableRegex'],
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
+ areScopedVariablesAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
environments: {
type: Array,
required: false,
default: () => [],
},
- hasEnvScopeQuery: {
+ hideEnvironmentScope: {
type: Boolean,
- required: true,
+ required: false,
+ default: false,
},
mode: {
type: String,
@@ -85,22 +117,107 @@ export default {
return VARIABLE_ACTIONS.includes(val);
},
},
+ selectedVariable: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
data() {
return {
- key: defaultVariableState.key,
- variableType: defaultVariableState.variableType,
+ variable: { ...defaultVariableState, ...this.selectedVariable },
+ trackedValidationErrorProperty: undefined,
};
},
computed: {
+ isValueMaskable() {
+ return this.variable.masked && !this.isValueMasked;
+ },
+ isValueMasked() {
+ const regex = RegExp(this.maskedRegexToUse);
+ return regex.test(this.variable.value);
+ },
+ canSubmit() {
+ return this.variable.key.length > 0 && this.isValueValid;
+ },
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
+ hasVariableReference() {
+ return this.isExpanded && VARIABLE_REFERENCE_REGEX.test(this.variable.value);
+ },
+ isExpanded() {
+ return !this.variable.raw;
+ },
+ isMaskedReqsMet() {
+ return !this.variable.masked || this.isValueMasked;
+ },
+ isValueEmpty() {
+ return this.variable.value === '';
+ },
+ isValueValid() {
+ return this.isValueEmpty || this.isMaskedReqsMet;
+ },
+ isEditing() {
+ return this.mode === EDIT_VARIABLE_ACTION;
+ },
+ maskedRegexToUse() {
+ return this.variable.raw ? this.maskableRawRegex : this.maskableRegex;
+ },
+ maskedReqsNotMetText() {
+ return !this.isMaskedReqsMet ? this.$options.i18n.valueFeedback.maskedReqsNotMet : '';
+ },
+ modalActionText() {
+ return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
+ },
+ },
+ watch: {
+ variable: {
+ handler() {
+ this.trackVariableValidationErrors();
+ },
+ deep: true,
+ },
+ },
+ mounted() {
+ if (this.isProtectedByDefault && !this.isEditing) {
+ this.variable = { ...this.variable, protected: true };
+ }
},
methods: {
close() {
this.$emit('close-form');
},
+ getTrackingErrorProperty() {
+ if (this.isValueEmpty) {
+ return null;
+ }
+
+ let property;
+ if (this.isValueMaskable) {
+ const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
+ const regex = new RegExp(supportedChars, 'g');
+ property = this.variable.value.replace(regex, '');
+ } else if (this.hasVariableReference) {
+ property = '$';
+ }
+
+ return property;
+ },
+ setRaw(expanded) {
+ this.variable = { ...this.variable, raw: !expanded };
+ },
+ submit() {
+ this.$emit(this.isEditing ? 'update-variable' : 'add-variable', this.variable);
+ this.close();
+ },
+ trackVariableValidationErrors() {
+ const property = this.getTrackingErrorProperty();
+ if (property && !this.trackedValidationErrorProperty) {
+ this.track(EVENT_ACTION, { property });
+ this.trackedValidationErrorProperty = property;
+ }
+ },
},
awsTokenList,
flagLink: helpPagePath('ci/variables/index', {
@@ -119,20 +236,25 @@ export default {
@close="close"
>
<template #title>
- <h2 class="gl-m-0">{{ $options.i18n.addVariable }}</h2>
+ <h2 class="gl-m-0">{{ modalActionText }}</h2>
</template>
<gl-form-group
:label="$options.i18n.type"
label-for="ci-variable-type"
- class="gl-border-none gl-mb-n5"
+ class="gl-border-none"
+ :class="{
+ 'gl-mb-n5': !hideEnvironmentScope,
+ 'gl-mb-n1': hideEnvironmentScope,
+ }"
>
<gl-form-select
id="ci-variable-type"
- v-model="variableType"
+ v-model="variable.variableType"
:options="$options.variableOptions"
/>
</gl-form-group>
<gl-form-group
+ v-if="!hideEnvironmentScope"
class="gl-border-none gl-mb-n5"
label-for="ci-variable-env"
data-testid="environment-scope"
@@ -154,11 +276,18 @@ export default {
</div>
</template>
<ci-environments-dropdown
+ v-if="areScopedVariablesAvailable"
class="gl-mb-5"
+ has-env-scope-query
:are-environments-loading="areEnvironmentsLoading"
:environments="environments"
- :has-env-scope-query="hasEnvScopeQuery"
- selected-environment-scope=""
+ :selected-environment-scope="variable.environmentScope"
+ />
+ <gl-form-input
+ v-else
+ :value="$options.i18n.defaultScope"
+ class="gl-w-full gl-mb-5"
+ readonly
/>
</gl-form-group>
<gl-form-group class="gl-border-none gl-mb-n8">
@@ -177,17 +306,21 @@ export default {
</gl-link>
</div>
</template>
- <gl-form-checkbox data-testid="ci-variable-protected-checkbox">
+ <gl-form-checkbox v-model="variable.protected" data-testid="ci-variable-protected-checkbox">
{{ $options.i18n.protectedField }}
<p class="gl-text-secondary">
{{ $options.i18n.protectedDescription }}
</p>
</gl-form-checkbox>
- <gl-form-checkbox data-testid="ci-variable-masked-checkbox">
+ <gl-form-checkbox v-model="variable.masked" data-testid="ci-variable-masked-checkbox">
{{ $options.i18n.maskedField }}
<p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
</gl-form-checkbox>
- <gl-form-checkbox data-testid="ci-variable-expanded-checkbox">
+ <gl-form-checkbox
+ data-testid="ci-variable-expanded-checkbox"
+ :checked="isExpanded"
+ @change="setRaw"
+ >
{{ $options.i18n.expandedField }}
<p class="gl-text-secondary">
<gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
@@ -199,34 +332,56 @@ export default {
</gl-form-checkbox>
</gl-form-group>
<gl-form-combobox
- v-model="key"
+ v-model="variable.key"
:token-list="$options.awsTokenList"
:label-text="$options.i18n.key"
class="gl-border-none gl-pb-0! gl-mb-n5"
- data-testid="pipeline-form-ci-variable-key"
+ data-testid="ci-variable-key"
data-qa-selector="ci_variable_key_field"
/>
<gl-form-group
:label="$options.i18n.value"
label-for="ci-variable-value"
class="gl-border-none gl-mb-n2"
+ data-testid="ci-variable-value-label"
+ :invalid-feedback="maskedReqsNotMetText"
+ :state="isValueValid"
>
<gl-form-textarea
id="ci-variable-value"
+ v-model="variable.value"
class="gl-border-none gl-font-monospace!"
rows="3"
max-rows="10"
- data-testid="pipeline-form-ci-variable-value"
+ data-testid="ci-variable-value"
data-qa-selector="ci_variable_value_field"
spellcheck="false"
/>
+ <p v-if="variable.raw" class="gl-mt-2 gl-mb-0 text-secondary" data-testid="raw-variable-tip">
+ {{ $options.i18n.valueFeedback.rawHelpText }}
+ </p>
</gl-form-group>
+ <gl-alert
+ v-if="hasVariableReference"
+ :title="$options.i18n.variableReferenceTitle"
+ :dismissible="false"
+ variant="warning"
+ class="gl-mx-4 gl-pl-9! gl-border-bottom-0"
+ data-testid="has-variable-reference-alert"
+ >
+ {{ $options.i18n.variableReferenceDescription }}
+ </gl-alert>
<div class="gl-display-flex gl-justify-content-end">
- <gl-button category="primary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
>{{ $options.i18n.cancel }}
</gl-button>
- <gl-button category="primary" variant="confirm" data-testid="confirm-button"
- >{{ $options.i18n.addVariable }}
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-btn"
+ @click="submit"
+ >{{ modalActionText }}
</gl-button>
</div>
</gl-drawer>
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 f4e1da9b34f..482f6da5617 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
@@ -139,7 +139,10 @@ export default {
<ci-variable-drawer
v-if="showDrawer"
:are-environments-loading="areEnvironmentsLoading"
- :has-env-scope-query="hasEnvScopeQuery"
+ :are-scoped-variables-available="areScopedVariablesAvailable"
+ :environments="environments"
+ :hide-environment-scope="hideEnvironmentScope"
+ :selected-variable="selectedVariable"
:mode="mode"
v-on="$listeners"
@close-form="closeForm"
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index 9786f25ed87..3d5ed327dc7 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
@@ -2,7 +2,8 @@
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
+import { reportMessageToSentry } from '~/ci/utils';
+import { mapEnvironmentNames } from '../utils';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
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 a14cd1e387a..3d62313815c 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
@@ -32,24 +32,20 @@ export default {
label: s__('CiVariables|Key'),
tdClass: 'text-plain',
sortable: true,
+ thClass: 'gl-w-40p',
},
{
key: 'value',
label: s__('CiVariables|Value'),
},
{
- key: 'Attributes',
- label: s__('CiVariables|Attributes'),
- thClass: 'gl-w-40p',
- },
- {
key: 'environmentScope',
label: s__('CiVariables|Environments'),
},
{
key: 'actions',
label: __('Actions'),
- thClass: 'gl-text-right',
+ thClass: 'gl-text-right gl-w-15',
},
],
inheritedVarsFields: [
@@ -287,7 +283,7 @@ export default {
</template>
<template #cell(key)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<span
:id="`ci-variable-key-${item.id}`"
@@ -298,16 +294,28 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
+ class="gl-my-n2 gl-ml-2"
+ size="small"
:title="__('Copy key')"
:data-clipboard-text="item.key"
:aria-label="__('Copy to clipboard')"
/>
</div>
+ <div data-testid="ci-variable-table-row-attributes" class="gl-mt-2">
+ <gl-badge
+ v-for="attribute in item.attributes"
+ :key="`${item.key}-${attribute}`"
+ class="gl-mr-2"
+ variant="info"
+ size="sm"
+ >
+ {{ attribute }}
+ </gl-badge>
+ </div>
</template>
<template v-if="!isInheritedGroupVars" #cell(value)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<span v-if="areValuesHidden" data-testid="hiddenValue">*****</span>
<span
@@ -321,29 +329,17 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
+ class="gl-my-n2 gl-ml-2"
+ size="small"
:title="__('Copy value')"
:data-clipboard-text="item.value"
:aria-label="__('Copy to clipboard')"
/>
</div>
</template>
- <template #cell(attributes)="{ item }">
- <span data-testid="ci-variable-table-row-attributes">
- <gl-badge
- v-for="attribute in item.attributes"
- :key="`${item.key}-${attribute}`"
- class="gl-mr-2"
- variant="info"
- size="sm"
- >
- {{ attribute }}
- </gl-badge>
- </span>
- </template>
<template #cell(environmentScope)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<span
:id="`ci-variable-env-${item.id}`"
@@ -354,7 +350,8 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
+ class="gl-my-n2 gl-ml-2"
+ size="small"
:title="__('Copy environment')"
:data-clipboard-text="convertEnvironmentScopeValue(item.environmentScope)"
:aria-label="__('Copy to clipboard')"
@@ -363,7 +360,7 @@ export default {
</template>
<template v-if="isInheritedGroupVars" #cell(group)="{ item }">
<div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-md-justify-content-start gl-mr-n3"
>
<gl-link
:id="`ci-variable-group-${item.id}`"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index 825b39e0cf9..fc37b62299d 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -46,6 +46,7 @@ export const AWS_TIP_MESSAGE = s__(
);
export const EVENT_LABEL = 'ci_variable_modal';
+export const DRAWER_EVENT_LABEL = 'ci_variable_drawer';
export const EVENT_ACTION = 'validation_error';
// AWS TOKEN CONSTANTS
diff --git a/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js
deleted file mode 100644
index fdbefd8c313..00000000000
--- a/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import $ from 'jquery';
-import VariableList from './ci_variable_list';
-
-// Used for the variable list on scheduled pipeline edit page
-export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
- const $container = $(container);
-
- const variableList = new VariableList({
- container: $container,
- formField,
- });
- variableList.init();
-
- // Clear out the names in the empty last row so it
- // doesn't get submitted and throw validation errors
- $container.closest('form').on('submit trigger-submit', () => {
- const $lastRow = $container.find('.js-row').last();
-
- const isTouched = variableList.checkIfRowTouched($lastRow);
- if (!isTouched) {
- $lastRow.find('input, textarea').attr('name', '');
- $lastRow.find('select').attr('name', '');
- }
- });
-}
diff --git a/app/assets/javascripts/ci/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_variable_list/utils.js
index eeca69274ce..1faa97a5f73 100644
--- a/app/assets/javascripts/ci/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_variable_list/utils.js
@@ -1,4 +1,3 @@
-import * as Sentry from '@sentry/browser';
import { uniq } from 'lodash';
import { allEnvironments } from './constants';
@@ -49,12 +48,3 @@ export const convertEnvironmentScope = (environmentScope = '') => {
export const mapEnvironmentNames = (nodes = []) => {
return nodes.map((env) => env.name);
};
-
-export const reportMessageToSentry = (component, message, context) => {
- Sentry.withScope((scope) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- scope.setContext('Vue data', context);
- scope.setTag('component', component);
- Sentry.captureMessage(message);
- });
-};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index c03085e6419..807128d2341 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -4,16 +4,16 @@ import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '../../event_hub';
-import { TRACKING_CATEGORIES } from '../../constants';
-import PipelineOperations from './pipeline_operations.vue';
-import PipelineStopModal from './pipeline_stop_modal.vue';
-import PipelineTriggerer from './pipeline_triggerer.vue';
-import PipelineUrl from './pipeline_url.vue';
-import PipelinesStatusBadge from './pipelines_status_badge.vue';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
+import eventHub from '~/ci/event_hub';
+import PipelineOperations from '../pipelines_page/components/pipeline_operations.vue';
+import PipelineStopModal from '../pipelines_page/components/pipeline_stop_modal.vue';
+import PipelineTriggerer from '../pipelines_page/components/pipeline_triggerer.vue';
+import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
+import PipelinesStatusBadge from '../pipelines_page/components/pipelines_status_badge.vue';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
@@ -95,10 +95,10 @@ export default {
},
{
key: 'triggerer',
- label: s__('Pipeline|Triggerer'),
+ label: s__('Pipeline|Created by'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
- columnClass: 'gl-w-10p',
+ columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
@@ -113,7 +113,7 @@ export default {
key: 'actions',
thClass: DEFAULT_TH_CLASSES,
tdClass: this.tdClasses,
- columnClass: 'gl-w-15p',
+ columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
},
];
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/ci/common/private/job_action_component.vue
index ffb6ab71b22..f649750ce8a 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_action_component.vue
@@ -5,7 +5,7 @@ import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
-import { reportToSentry } from '../../utils';
+import { reportToSentry } from '~/ci/utils';
/**
* Renders either a cancel, retry or play icon button and handles the post request
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/ci/common/private/job_links_layer.vue
index ef24694e494..59260ca3f81 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ b/app/assets/javascripts/ci/common/private/job_links_layer.vue
@@ -1,8 +1,8 @@
<script>
import { memoize } from 'lodash';
-import { reportToSentry } from '../../utils';
-import { parseData } from '../parsing_utils';
-import LinksInner from './links_inner.vue';
+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';
const parseForLinksBare = (pipeline) => {
const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index 1c7f5a7476d..1c7f5a7476d 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue
new file mode 100644
index 00000000000..86ccdb2c87b
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/app.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlFilteredSearch } from '@gitlab/ui';
+import {
+ OPERATOR_IS,
+ OPERATORS_IS,
+ TOKEN_TITLE_STATUS,
+ TOKEN_TYPE_STATUS,
+ TOKEN_TITLE_JOBS_RUNNER_TYPE,
+ TOKEN_TYPE_JOBS_RUNNER_TYPE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import JobStatusToken from './tokens/job_status_token.vue';
+import JobRunnerTypeToken from './tokens/job_runner_type_token.vue';
+
+export default {
+ components: {
+ GlFilteredSearch,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ queryString: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ tokens() {
+ const tokens = [
+ {
+ type: TOKEN_TYPE_STATUS,
+ icon: 'status',
+ title: TOKEN_TITLE_STATUS,
+ unique: true,
+ token: JobStatusToken,
+ operators: OPERATORS_IS,
+ },
+ ];
+
+ if (this.glFeatures.adminJobsFilterRunnerType) {
+ tokens.push({
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ title: TOKEN_TITLE_JOBS_RUNNER_TYPE,
+ unique: true,
+ token: JobRunnerTypeToken,
+ operators: OPERATORS_IS,
+ });
+ }
+
+ return tokens;
+ },
+ filteredSearchValue() {
+ return Object.entries(this.queryString || {}).reduce(
+ (acc, [queryStringKey, queryStringValue]) => {
+ switch (queryStringKey) {
+ case 'statuses':
+ return [
+ ...acc,
+ {
+ type: TOKEN_TYPE_STATUS,
+ value: { data: queryStringValue, operator: OPERATOR_IS },
+ },
+ ];
+ case 'runnerTypes':
+ if (!this.glFeatures.adminJobsFilterRunnerType) {
+ return acc;
+ }
+
+ return [
+ ...acc,
+ {
+ type: TOKEN_TYPE_JOBS_RUNNER_TYPE,
+ value: { data: queryStringValue, operator: OPERATOR_IS },
+ },
+ ];
+ default:
+ return acc;
+ }
+ },
+ [],
+ );
+ },
+ },
+ methods: {
+ onSubmit(filters) {
+ this.$emit('filterJobsBySearch', filters);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search
+ :placeholder="s__('Jobs|Filter jobs')"
+ :available-tokens="tokens"
+ :value="filteredSearchValue"
+ @submit="onSubmit"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js b/app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js
new file mode 100644
index 00000000000..86b8290864c
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/constants.js
@@ -0,0 +1,23 @@
+export const jobStatusValues = [
+ 'CANCELED',
+ 'CREATED',
+ 'FAILED',
+ 'MANUAL',
+ 'SUCCESS',
+ 'PENDING',
+ 'PREPARING',
+ 'RUNNING',
+ 'SCHEDULED',
+ 'SKIPPED',
+ 'WAITING_FOR_RESOURCE',
+];
+
+export const JOB_RUNNER_TYPE_INSTANCE_TYPE = 'INSTANCE_TYPE';
+export const JOB_RUNNER_TYPE_GROUP_TYPE = 'GROUP_TYPE';
+export const JOB_RUNNER_TYPE_PROJECT_TYPE = 'PROJECT_TYPE';
+
+export const jobRunnerTypeValues = [
+ JOB_RUNNER_TYPE_INSTANCE_TYPE,
+ JOB_RUNNER_TYPE_GROUP_TYPE,
+ JOB_RUNNER_TYPE_PROJECT_TYPE,
+];
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue
new file mode 100644
index 00000000000..5bd3693b4d9
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_runner_type_token.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import {
+ JOB_RUNNER_TYPE_INSTANCE_TYPE,
+ JOB_RUNNER_TYPE_GROUP_TYPE,
+ JOB_RUNNER_TYPE_PROJECT_TYPE,
+} from '../constants';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ runnerTypes() {
+ return [
+ {
+ class: 'ci-runner-runner-type-instance',
+ icon: 'users',
+ text: s__('Runners|Instance'),
+ value: JOB_RUNNER_TYPE_INSTANCE_TYPE,
+ },
+ {
+ class: 'ci-runner-runner-type-group',
+ icon: 'group',
+ text: s__('Runners|Group'),
+ value: JOB_RUNNER_TYPE_GROUP_TYPE,
+ },
+ {
+ class: 'ci-runner-runner-type-project',
+ icon: 'project',
+ text: s__('Runners|Project'),
+ value: JOB_RUNNER_TYPE_PROJECT_TYPE,
+ },
+ ];
+ },
+ findActiveRunnerType() {
+ return this.runnerTypes.find((runnerType) => runnerType.value === this.value.data);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
+ <template #view>
+ <div class="gl-display-flex gl-align-items-center">
+ <div :class="findActiveRunnerType.class">
+ <gl-icon :name="findActiveRunnerType.icon" class="gl-mr-2 gl-display-block" />
+ </div>
+ <span>{{ findActiveRunnerType.text }}</span>
+ </div>
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="(runnerType, index) in runnerTypes"
+ :key="index"
+ :value="runnerType.value"
+ >
+ <div class="gl-display-flex" :class="runnerType.class">
+ <gl-icon :name="runnerType.icon" class="gl-mr-3" />
+ <span>{{ runnerType.text }}</span>
+ </div>
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue
index aad86ded80a..aad86ded80a 100644
--- a/app/assets/javascripts/jobs/components/filtered_search/tokens/job_status_token.vue
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/tokens/job_status_token.vue
diff --git a/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js b/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js
new file mode 100644
index 00000000000..43c0da72d3d
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/jobs_filtered_search/utils.js
@@ -0,0 +1,22 @@
+import { jobStatusValues, jobRunnerTypeValues } from './constants';
+
+// validates query string used for filtered search
+// on jobs table to ensure GraphQL query is called correctly
+export const validateQueryString = (queryStringObj) => {
+ return Object.entries(queryStringObj).reduce((acc, [queryStringKey, queryStringValue]) => {
+ switch (queryStringKey) {
+ case 'statuses': {
+ const statusValue = queryStringValue.toUpperCase();
+ const statusValueValid = jobStatusValues.includes(statusValue);
+ return statusValueValid ? { ...acc, statuses: statusValue } : acc;
+ }
+ case 'runnerTypes': {
+ const runnerTypesValue = queryStringValue.toUpperCase();
+ const runnerTypesValueValid = jobRunnerTypeValues.includes(runnerTypesValue);
+ return runnerTypesValueValid ? { ...acc, runnerTypes: runnerTypesValue } : acc;
+ }
+ default:
+ return acc;
+ }
+ }, null);
+};
diff --git a/app/assets/javascripts/ci/constants.js b/app/assets/javascripts/ci/constants.js
new file mode 100644
index 00000000000..93c2504dd5d
--- /dev/null
+++ b/app/assets/javascripts/ci/constants.js
@@ -0,0 +1,51 @@
+import { __, s__ } from '~/locale';
+
+export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
+
+export const BUTTON_TOOLTIP_RETRY = __('Retry all failed or cancelled jobs');
+export const BUTTON_TOOLTIP_CANCEL = __('Cancel the running pipeline');
+
+export const FILTER_TAG_IDENTIFIER = 'tag';
+
+export const JOB_GRAPHQL_ERRORS = {
+ jobMutationErrorText: __('There was an error running the job. Please try again.'),
+ jobQueryErrorText: __('There was an error fetching the job.'),
+};
+
+export const ICONS = {
+ TAG: 'tag',
+ MR: 'git-merge',
+ BRANCH: 'branch',
+ RETRY: 'retry',
+ SUCCESS: 'success',
+};
+
+export const SUCCESS_STATUS = 'SUCCESS';
+export const PASSED_STATUS = 'passed';
+export const MANUAL_STATUS = 'manual';
+
+// Constants for the ID and IID selection dropdown
+export const PipelineKeyOptions = [
+ {
+ text: __('Show Pipeline ID'),
+ label: __('Pipeline ID'),
+ value: 'id',
+ },
+ {
+ text: __('Show Pipeline IID'),
+ label: __('Pipeline IID'),
+ value: 'iid',
+ },
+];
+
+export const RAW_TEXT_WARNING = s__(
+ 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
+);
+
+export const TRACKING_CATEGORIES = {
+ table: 'pipelines_table_component',
+ tabs: 'pipelines_filter_tabs',
+ search: 'pipelines_filtered_search',
+ failed: 'pipeline_failed_jobs_tab',
+ tests: 'pipeline_tests_tab',
+};
diff --git a/app/assets/javascripts/jobs/components/table/event_hub.js b/app/assets/javascripts/ci/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/jobs/components/table/event_hub.js
+++ b/app/assets/javascripts/ci/event_hub.js
diff --git a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
index 27ee1b794f6..f02d59af1d9 100644
--- a/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
+++ b/app/assets/javascripts/ci/inherited_ci_variables/components/inherited_ci_variables_app.vue
@@ -3,7 +3,7 @@ import { produce } from 'immer';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '~/ci/ci_variable_list/utils';
+import { reportMessageToSentry } from '~/ci/utils';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import getInheritedCiVariables from '../graphql/queries/inherited_ci_variables.query.graphql';
diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/ci/job_details/components/empty_state.vue
index d0a39025807..5756d4a71df 100644
--- a/app/assets/javascripts/jobs/components/job/empty_state.vue
+++ b/app/assets/javascripts/ci/job_details/components/empty_state.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
+import ManualVariablesForm from '~/ci/job_details/components/manual_variables_form.vue';
export default {
components: {
diff --git a/app/assets/javascripts/jobs/components/job/environments_block.vue b/app/assets/javascripts/ci/job_details/components/environments_block.vue
index 4046e1ade82..4046e1ade82 100644
--- a/app/assets/javascripts/jobs/components/job/environments_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/environments_block.vue
diff --git a/app/assets/javascripts/jobs/components/job/erased_block.vue b/app/assets/javascripts/ci/job_details/components/erased_block.vue
index a815689659e..a815689659e 100644
--- a/app/assets/javascripts/jobs/components/job/erased_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/erased_block.vue
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 1adda905006..13f3eebd447 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,16 +4,9 @@ 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 './ci_badge_link.vue';
-import TimeagoTooltip from './time_ago_tooltip.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-/**
- * Renders header component for job and pipeline page based on UI mockups
- *
- * Used in:
- * - job show page
- * - pipeline show page
- */
export default {
components: {
CiBadgeLink,
@@ -33,33 +26,21 @@ export default {
type: Object,
required: true,
},
- itemName: {
+ name: {
type: String,
required: true,
},
- itemId: {
- type: String,
- required: false,
- default: '',
- },
time: {
type: String,
required: true,
},
user: {
type: Object,
- required: false,
- default: () => ({}),
- },
- hasSidebarButton: {
- type: Boolean,
- required: false,
- default: false,
+ required: true,
},
shouldRenderTriggeredLabel: {
type: Boolean,
- required: false,
- default: true,
+ required: true,
},
},
@@ -92,13 +73,6 @@ export default {
message() {
return this.user?.status?.message;
},
- item() {
- if (this.itemId) {
- return `${this.itemName} #${this.itemId}`;
- }
-
- return this.itemName;
- },
userId() {
return isGid(this.user?.id) ? getIdFromGraphQLId(this.user?.id) : this.user?.id;
},
@@ -114,13 +88,16 @@ export default {
</script>
<template>
- <header class="page-content-header gl-md-display-flex gl-min-h-7" data-testid="ci-header-content">
+ <header
+ class="page-content-header gl-md-display-flex gl-min-h-7"
+ data-testid="job-header-content"
+ >
<section class="header-main-content gl-mr-3">
<ci-badge-link class="gl-mr-3" :status="status" />
- <strong data-testid="ci-header-item-text">{{ item }}</strong>
+ <strong data-testid="job-name">{{ name }}</strong>
- <template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template>
+ <template v-if="shouldRenderTriggeredLabel">{{ __('started') }}</template>
<template v-else>{{ __('created') }}</template>
<timeago-tooltip :time="time" />
@@ -158,11 +135,10 @@ export default {
</section>
<!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
- <section v-if="$slots.default" data-testid="ci-header-action-buttons" class="gl-display-flex">
+ <section v-if="$slots.default" data-testid="job-header-action-buttons" class="gl-display-flex">
<slot></slot>
</section>
<gl-button
- v-if="hasSidebarButton"
class="gl-md-display-none gl-ml-auto gl-align-self-start js-sidebar-build-toggle"
icon="chevron-double-lg-left"
:aria-label="__('Toggle sidebar')"
diff --git a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
index efd4eed2a9f..419efcba46d 100644
--- a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink, GlButton, GlSearchBoxByClick } from '@gitla
import { scrollToElement, backOff } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __, s__, sprintf } from '~/locale';
+import { compactJobLog } from '~/ci/job_details/utils';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -130,17 +131,7 @@ export default {
if (!this.searchTerm) return;
- const compactedLog = [];
-
- this.jobLog.forEach((obj) => {
- if (obj.lines && obj.lines.length > 0) {
- compactedLog.push(...obj.lines);
- }
-
- if (!obj.lines && obj.content.length > 0) {
- compactedLog.push(obj);
- }
- });
+ const compactedLog = compactJobLog(this.jobLog);
compactedLog.forEach((line) => {
const lineText = line.content[0].text;
@@ -155,7 +146,7 @@ export default {
// BE returns zero based index, we need to add one to match the line numbers in the DOM
const firstSearchResult = `#L${this.searchResults[0].lineNumber + 1}`;
- const logLine = document.querySelector(`.js-line ${firstSearchResult}`);
+ const logLine = document.querySelector(`.log-line ${firstSearchResult}`);
if (logLine) {
setTimeout(() => scrollToElement(logLine));
@@ -224,7 +215,7 @@ export default {
:aria-label="$options.i18n.showRawButtonLabel"
:href="rawPath"
data-testid="job-raw-link-controller"
- icon="doc-text"
+ icon="doc-code"
/>
<!-- eo links -->
diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
index 13716b4d391..39c612bc600 100644
--- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
@@ -27,11 +27,24 @@ export default {
badgeDuration() {
return this.section.line && this.section.line.section_duration;
},
+ highlightedLines() {
+ return this.searchResults.map((result) => result.lineNumber);
+ },
+ headerIsHighlighted() {
+ const {
+ line: { lineNumber },
+ } = this.section;
+
+ return this.highlightedLines.includes(lineNumber);
+ },
},
methods: {
handleOnClickCollapsibleLine(section) {
this.$emit('onClickCollapsibleLine', section);
},
+ lineIsHighlighted({ lineNumber }) {
+ return this.highlightedLines.includes(lineNumber);
+ },
},
};
</script>
@@ -42,6 +55,7 @@ export default {
:duration="badgeDuration"
:path="jobLogEndpoint"
:is-closed="section.isClosed"
+ :is-highlighted="headerIsHighlighted"
@toggleLine="handleOnClickCollapsibleLine(section)"
/>
<template v-if="!section.isClosed">
@@ -50,7 +64,7 @@ export default {
:key="line.offset"
:line="line"
:path="jobLogEndpoint"
- :search-results="searchResults"
+ :is-highlighted="lineIsHighlighted(line)"
/>
</template>
</div>
diff --git a/app/assets/javascripts/jobs/components/log/duration_badge.vue b/app/assets/javascripts/ci/job_details/components/log/duration_badge.vue
index 54b76fd9edd..54b76fd9edd 100644
--- a/app/assets/javascripts/jobs/components/log/duration_badge.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/duration_badge.vue
diff --git a/app/assets/javascripts/jobs/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue
index 3c9c5097122..fa4a12b3dd3 100644
--- a/app/assets/javascripts/jobs/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { getLocationHash } from '~/lib/utils/url_utility';
-import { linkRegex } from '../../utils';
+import { linkRegex } from './utils';
import LineNumber from './line_number.vue';
export default {
@@ -15,14 +15,14 @@ export default {
type: String,
required: true,
},
- searchResults: {
- type: Array,
+ isHighlighted: {
+ type: Boolean,
required: false,
- default: () => [],
+ default: false,
},
},
render(h, { props }) {
- const { line, path, searchResults } = props;
+ const { line, path, isHighlighted } = props;
const chars = line.content.map((content) => {
return h(
@@ -52,31 +52,21 @@ export default {
);
});
- let applyHighlight = false;
-
- if (searchResults.length > 0) {
- const linesToHighlight = searchResults.map((searchResultLine) => searchResultLine.lineNumber);
-
- linesToHighlight.forEach((num) => {
- if (num === line.lineNumber) {
- applyHighlight = true;
- }
- });
- }
+ let applyHashHighlight = false;
if (window.location.hash) {
const hash = getLocationHash();
const lineToMatch = `L${line.lineNumber + 1}`;
if (hash === lineToMatch) {
- applyHighlight = true;
+ applyHashHighlight = true;
}
}
return h(
'div',
{
- class: ['js-line', 'log-line', applyHighlight ? 'gl-bg-gray-700' : ''],
+ class: ['js-line', 'log-line', { 'gl-bg-gray-700': isHighlighted || applyHashHighlight }],
},
[
h(LineNumber, {
diff --git a/app/assets/javascripts/jobs/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index 115b090b32a..e647ab4ac0b 100644
--- a/app/assets/javascripts/jobs/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -28,17 +28,29 @@ export default {
required: false,
default: '',
},
+ isHighlighted: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ applyHashHighlight: false,
+ };
},
computed: {
iconName() {
return this.isClosed ? 'chevron-lg-right' : 'chevron-lg-down';
},
- applyHighlight() {
- const hash = getLocationHash();
- const lineToMatch = `L${this.line.lineNumber + 1}`;
+ },
+ mounted() {
+ const hash = getLocationHash();
+ const lineToMatch = `L${this.line.lineNumber + 1}`;
- return hash === lineToMatch;
- },
+ if (hash === lineToMatch) {
+ this.applyHashHighlight = true;
+ }
},
methods: {
handleOnClick() {
@@ -50,12 +62,12 @@ export default {
<template>
<div
- class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start"
- :class="{ 'gl-bg-gray-700': applyHighlight }"
+ class="log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
+ :class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }"
role="button"
@click="handleOnClick"
>
- <gl-icon :name="iconName" class="arrow position-absolute" />
+ <gl-icon :name="iconName" class="arrow gl-absolute gl-top-2" />
<line-number :line-number="line.lineNumber" :path="path" />
<span
v-for="(content, i) in line.content"
diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
index 7ca9154d2fe..7ca9154d2fe 100644
--- a/app/assets/javascripts/jobs/components/log/line_number.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
diff --git a/app/assets/javascripts/jobs/components/log/log.vue b/app/assets/javascripts/ci/job_details/components/log/log.vue
index 6a1101bf297..fb6a6a58074 100644
--- a/app/assets/javascripts/jobs/components/log/log.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/log.vue
@@ -26,6 +26,9 @@ export default {
'isJobLogComplete',
'isScrolledToBottomBeforeReceivingJobLog',
]),
+ highlightedLines() {
+ return this.searchResults.map((result) => result.lineNumber);
+ },
},
updated() {
this.$nextTick(() => {
@@ -68,11 +71,14 @@ export default {
}, 0);
}
},
+ isHighlighted({ lineNumber }) {
+ return this.highlightedLines.includes(lineNumber);
+ },
},
};
</script>
<template>
- <code class="job-log d-block" data-qa-selector="job_log_content">
+ <code class="job-log d-block" data-testid="job-log-content">
<template v-for="(section, index) in jobLog">
<collapsible-log-section
v-if="section.isHeader"
@@ -87,7 +93,7 @@ export default {
:key="section.offset"
:line="section"
:path="jobLogEndpoint"
- :search-results="searchResults"
+ :is-highlighted="isHighlighted(section)"
/>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/utils.js b/app/assets/javascripts/ci/job_details/components/log/utils.js
new file mode 100644
index 00000000000..1ccecf3eb53
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/log/utils.js
@@ -0,0 +1,12 @@
+/**
+ * capture anything starting with http:// or https://
+ * https?:\/\/
+ *
+ * up until a disallowed character or whitespace
+ * [^"<>()\\^`{|}\s]+
+ *
+ * and a disallowed character or whitespace, including non-ending chars .,:;!?
+ * [^"<>()\\^`{|}\s.,:;!?]
+ */
+export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
+export default { linkRegex };
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
index 356d65e1d14..1232ffffb57 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
@@ -14,16 +14,16 @@ import { fetchPolicies } from '~/lib/graphql';
import { createAlert } from '~/alert';
import { TYPENAME_CI_BUILD, TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { JOB_GRAPHQL_ERRORS } from '~/jobs/constants';
+import { JOB_GRAPHQL_ERRORS } from '~/ci/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { s__ } from '~/locale';
-import { reportMessageToSentry } from '~/jobs/utils';
-import GetJob from './graphql/queries/get_job.query.graphql';
-import playJobWithVariablesMutation from './graphql/mutations/job_play_with_variables.mutation.graphql';
-import retryJobWithVariablesMutation from './graphql/mutations/job_retry_with_variables.mutation.graphql';
+import { reportMessageToSentry } from '~/ci/utils';
+import GetJob from '../graphql/queries/get_job.query.graphql';
+import playJobWithVariablesMutation from '../graphql/mutations/job_play_with_variables.mutation.graphql';
+import retryJobWithVariablesMutation from '../graphql/mutations/job_retry_with_variables.mutation.graphql';
-// This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue
+// This component is a port of ~/ci/job_details/components/legacy_manual_variables_form.vue
// It is meant to fetch/update the job information via GraphQL instead of REST API.
export default {
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
index a78cacf110f..4c81a9bd033 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/artifacts_block.vue
@@ -75,7 +75,7 @@ export default {
data-testid="artifacts-remove-timeline"
>
<span v-if="isExpired">{{ $options.i18n.expiredText }}</span>
- <span v-if="willExpire" data-qa-selector="artifacts_unlocked_message_content">
+ <span v-if="willExpire" data-testid="artifacts-unlocked-message-content">
{{ $options.i18n.willExpireText }}
</span>
<timeago-tooltip v-if="artifact.expire_at" :time="artifact.expire_at" />
@@ -89,7 +89,7 @@ export default {
</gl-link>
</p>
<p v-else-if="isLocked" class="build-detail-row">
- <span data-testid="job-locked-message" data-qa-selector="artifacts_locked_message_content">
+ <span data-testid="artifacts-locked-message-content">
{{ $options.i18n.lockedText }}
</span>
</p>
@@ -112,8 +112,7 @@ export default {
<gl-button
v-if="artifact.browse_path"
:href="artifact.browse_path"
- data-testid="browse-artifacts"
- data-qa-selector="browse_artifacts_button"
+ data-testid="browse-artifacts-button"
>{{ $options.i18n.browseText }}</gl-button
>
</gl-button-group>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
new file mode 100644
index 00000000000..95616a4c706
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/commit_block.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ GlLink,
+ },
+ props: {
+ commit: {
+ type: Object,
+ required: true,
+ },
+ mergeRequest: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <p class="gl-display-flex gl-flex-wrap gl-align-items-baseline gl-gap-2 gl-mb-0">
+ <span class="gl-display-flex gl-font-weight-bold">{{ __('Commit') }}</span>
+
+ <gl-link
+ :href="commit.commit_path"
+ class="gl-text-blue-500! gl-font-monospace"
+ data-testid="commit-sha"
+ >
+ {{ commit.short_id }}
+ </gl-link>
+
+ <clipboard-button
+ :text="commit.id"
+ :title="__('Copy commit SHA')"
+ category="tertiary"
+ size="small"
+ class="gl-align-self-center"
+ />
+
+ <span v-if="mergeRequest">
+ {{ __('in') }}
+ <gl-link :href="mergeRequest.path" class="gl-text-blue-500!" data-testid="link-commit"
+ >!{{ mergeRequest.iid }}</gl-link
+ >
+ </span>
+ </p>
+
+ <p class="gl-mb-0">{{ commit.title }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue
new file mode 100644
index 00000000000..a87f4b8467e
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/external_links_block.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ externalLinks: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div class="title gl-font-weight-bold">{{ s__('Job|External links') }}</div>
+ <ul class="gl-list-style-none gl-p-0 gl-m-0">
+ <li v-for="(externalLink, index) in externalLinks" :key="index">
+ <gl-link
+ :href="externalLink.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="gl-text-blue-600!"
+ >
+ <gl-icon name="external-link" class="flex-shrink-0" />
+ {{ externalLink.label }}
+ </gl-link>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 097ab3b4cf6..8e87f118fa4 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -40,7 +40,7 @@ export default {
},
classes() {
return {
- retried: this.job.retried,
+ 'retried gl-text-secondary': this.job.retried,
'gl-font-weight-bold': this.isActive,
};
},
@@ -57,7 +57,7 @@ export default {
v-gl-tooltip.left.viewport
:href="job.status.details_path"
:title="tooltipText"
- class="gl-display-flex gl-align-items-center"
+ class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7"
:data-testid="dataTestId"
>
<gl-icon
@@ -67,11 +67,11 @@ export default {
:size="14"
/>
- <ci-icon :status="job.status" class="gl-mr-2" :size="14" />
+ <ci-icon :status="job.status" class="gl-mr-3" :size="14" />
<span class="gl-text-truncate gl-w-full">{{ jobName }}</span>
- <gl-icon v-if="job.retried" name="retry" />
+ <gl-icon v-if="job.retried" name="retry" class="gl-mr-4" />
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue
index a3f1a2c4be8..58e49c71830 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_retry_forward_deployment_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlModal } from '@gitlab/ui';
-import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants';
+import { __, s__ } from '~/locale';
export default {
name: 'JobRetryForwardDeploymentModal',
@@ -9,7 +9,15 @@ export default {
GlModal,
},
i18n: {
- ...JOB_RETRY_FORWARD_DEPLOYMENT_MODAL,
+ cancel: __('Cancel'),
+ info: s__(
+ `Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment.
+ Retrying this job could result in overwriting the environment with the older source code.`,
+ ),
+ areYouSure: s__('Jobs|Are you sure you want to proceed?'),
+ moreInfo: __('More information'),
+ primaryText: __('Retry job'),
+ title: s__('Jobs|Are you sure you want to retry this job?'),
},
inject: {
retryOutdatedJobDocsUrl: {
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_sidebar_retry_button.vue
index 87c47f592aa..26676123dc3 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_sidebar_retry_button.vue
@@ -2,12 +2,14 @@
import { GlButton, GlDisclosureDropdown, GlModalDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
-import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
+import { s__ } from '~/locale';
export default {
name: 'JobSidebarRetryButton',
i18n: {
- ...JOB_SIDEBAR_COPY,
+ retryJobLabel: s__('Job|Retry'),
+ runAgainJobButtonLabel: s__('Job|Run again'),
+ updateVariables: s__('Job|Update CI/CD variables'),
},
components: {
GlButton,
@@ -65,6 +67,7 @@ export default {
icon="retry"
category="primary"
placement="right"
+ positioning-strategy="fixed"
variant="confirm"
:items="dropdownItems"
/>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/jobs_container.vue
index df64b6422c7..18bd2593c2a 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/jobs_container.vue
@@ -24,7 +24,8 @@ export default {
};
</script>
<template>
- <div class="builds-container">
+ <div class="block builds-container">
+ <b class="gl-display-flex gl-mb-2 gl-font-weight-semibold">{{ __('Related jobs') }}</b>
<job-container-item
v-for="job in jobs"
:key="job.id"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 92e1557ada2..7f2f4fc0331 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -1,11 +1,12 @@
<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import { forwardDeploymentFailureModalId } from '~/ci/constants';
+import { filterAnnotations } from '~/ci/job_details/utils';
import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_block.vue';
+import ExternalLinksBlock from './external_links_block.vue';
import JobsContainer from './jobs_container.vue';
import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue';
import JobSidebarDetailsContainer from './sidebar_job_details_container.vue';
@@ -15,22 +16,17 @@ import TriggerBlock from './trigger_block.vue';
export default {
name: 'JobSidebar',
- i18n: {
- ...JOB_SIDEBAR_COPY,
- },
- borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
forwardDeploymentFailureModalId,
components: {
ArtifactsBlock,
CommitBlock,
- GlButton,
- GlIcon,
JobsContainer,
JobRetryForwardDeploymentModal,
JobSidebarDetailsContainer,
SidebarHeader,
StagesDropdown,
TriggerBlock,
+ ExternalLinksBlock,
},
props: {
artifactHelpUrl: {
@@ -46,6 +42,9 @@ export default {
// the artifact object will always have a locked property
return Object.keys(this.job.artifact).length > 1;
},
+ hasExternalLinks() {
+ return this.externalLinks.length > 0;
+ },
hasTriggers() {
return !isEmpty(this.job.trigger);
},
@@ -58,6 +57,9 @@ export default {
shouldShowJobRetryForwardDeploymentModal() {
return this.job.retry_path && this.hasForwardDeploymentFailure;
},
+ externalLinks() {
+ return filterAnnotations(this.job.annotations, 'external_link');
+ },
},
watch: {
job(value, oldValue) {
@@ -77,73 +79,44 @@ export default {
<template>
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
- <div class="blocks-container">
+ <div class="blocks-container gl-p-4">
<sidebar-header
+ class="block gl-pb-4! gl-mb-2"
:rest-job="job"
:job-id="job.id"
@updateVariables="$emit('updateVariables')"
/>
- <div
- v-if="job.terminal_path || job.new_issue_path"
- class="gl-py-5"
- :class="$options.borderTopClass"
- >
- <gl-button
- v-if="job.new_issue_path"
- :href="job.new_issue_path"
- category="secondary"
- variant="confirm"
- data-testid="job-new-issue"
- >
- {{ $options.i18n.newIssue }}
- </gl-button>
- <gl-button
- v-if="job.terminal_path"
- :href="job.terminal_path"
- target="_blank"
- data-testid="terminal-link"
- >
- {{ $options.i18n.debug }}
- <gl-icon name="external-link" />
- </gl-button>
- </div>
- <job-sidebar-details-container class="gl-py-5" :class="$options.borderTopClass" />
+ <job-sidebar-details-container class="block gl-mb-2" />
<artifacts-block
v-if="hasArtifact"
- class="gl-py-5"
- :class="$options.borderTopClass"
+ class="block gl-mb-2"
:artifact="job.artifact"
:help-url="artifactHelpUrl"
/>
- <trigger-block
- v-if="hasTriggers"
- class="gl-py-5"
- :class="$options.borderTopClass"
- :trigger="job.trigger"
+ <external-links-block
+ v-if="hasExternalLinks"
+ class="block gl-mb-2"
+ :external-links="externalLinks"
/>
- <commit-block
- :commit="commit"
- class="gl-py-5"
- :class="$options.borderTopClass"
- :merge-request="job.merge_request"
- />
+ <trigger-block v-if="hasTriggers" class="block gl-mb-2" :trigger="job.trigger" />
+
+ <commit-block class="block gl-mb-2" :commit="commit" :merge-request="job.merge_request" />
<stages-dropdown
v-if="job.pipeline"
- class="gl-py-5"
- :class="$options.borderTopClass"
+ class="block gl-mb-2"
:pipeline="job.pipeline"
:selected-stage="selectedStage"
:stages="stages"
@requestSidebarStageDropdown="fetchJobsForStage"
/>
- </div>
- <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
+ <jobs-container v-if="jobs.length" :job-id="job.id" :jobs="jobs" />
+ </div>
</div>
<job-retry-forward-deployment-modal
v-if="shouldShowJobRetryForwardDeploymentModal"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
index 0ba34eafa58..5b1bf354fd4 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_detail_row.vue
@@ -39,21 +39,26 @@ export default {
};
</script>
<template>
- <p class="gl-display-flex gl-justify-content-space-between gl-mb-2">
- <span v-if="hasTitle">
- <b>{{ title }}:</b>
+ <p class="build-sidebar-item gl-mb-2">
+ <b v-if="hasTitle" class="gl-display-flex">{{ title }}:</b>
+ <gl-link
+ v-if="path"
+ :href="path"
+ class="gl-text-blue-600!"
+ data-testid="job-sidebar-value-link"
+ >
+ {{ value }}
+ </gl-link>
+ <span v-else
+ >{{ value }}
<gl-link
- v-if="path"
- :href="path"
- class="gl-text-blue-600!"
- data-testid="job-sidebar-value-link"
+ v-if="hasHelpURL"
+ :href="helpUrl"
+ target="_blank"
+ data-testid="job-sidebar-help-link"
>
- {{ value }}
+ <gl-icon name="question-o" class="gl-ml-2 gl-text-blue-500" />
</gl-link>
- <span v-else>{{ value }}</span>
</span>
- <gl-link v-if="hasHelpURL" :href="helpUrl" target="_blank" data-testid="job-sidebar-help-link">
- <gl-icon name="question-o" />
- </gl-link>
</p>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
index 56fcd8738d7..77e3ecb9b3c 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_header.vue
@@ -5,20 +5,23 @@ import { mapActions } from 'vuex';
import { createAlert } from '~/alert';
import { TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { __, s__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import {
- JOB_GRAPHQL_ERRORS,
- JOB_SIDEBAR_COPY,
- forwardDeploymentFailureModalId,
- PASSED_STATUS,
-} from '~/jobs/constants';
-import GetJob from '../graphql/queries/get_job.query.graphql';
+import { JOB_GRAPHQL_ERRORS, forwardDeploymentFailureModalId, PASSED_STATUS } from '~/ci/constants';
+import GetJob from '../../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
export default {
name: 'SidebarHeader',
i18n: {
- ...JOB_SIDEBAR_COPY,
+ cancelJobButtonLabel: s__('Job|Cancel'),
+ debug: __('Debug'),
+ eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
+ eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
+ newIssue: __('New issue'),
+ retryJobLabel: s__('Job|Retry'),
+ toggleSidebar: __('Toggle Sidebar'),
+ runAgainJobButtonLabel: s__('Job|Run again'),
},
forwardDeploymentFailureModalId,
directives: {
@@ -90,27 +93,47 @@ export default {
</script>
<template>
- <div class="gl-py-5 gl-display-flex gl-align-items-center">
+ <div>
<tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
+ ><h4 class="gl-mt-0 gl-mb-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4>
</tooltip-on-truncate>
- <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <div class="gl-display-flex gl-gap-3">
<gl-button
v-if="restJob.erase_path"
- v-gl-tooltip.left
+ v-gl-tooltip.bottom
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
:href="restJob.erase_path"
:data-confirm="$options.i18n.eraseLogConfirmText"
- class="gl-mr-2"
data-testid="job-log-erase-link"
data-confirm-btn-variant="danger"
data-method="post"
icon="remove"
/>
+ <gl-button
+ v-if="restJob.new_issue_path"
+ v-gl-tooltip.bottom
+ :href="restJob.new_issue_path"
+ :title="$options.i18n.newIssue"
+ :aria-label="$options.i18n.newIssue"
+ category="secondary"
+ variant="confirm"
+ data-testid="job-new-issue"
+ icon="issue-new"
+ />
+ <gl-button
+ v-if="restJob.terminal_path"
+ v-gl-tooltip.bottom
+ :href="restJob.terminal_path"
+ :title="$options.i18n.debug"
+ :aria-label="$options.i18n.debug"
+ target="_blank"
+ icon="external-link"
+ data-testid="terminal-link"
+ />
<job-sidebar-retry-button
v-if="canShowJobRetryButton"
- v-gl-tooltip.left
+ v-gl-tooltip.bottom
:title="buttonTitle"
:aria-label="buttonTitle"
:is-manual-job="isManualJob"
@@ -118,13 +141,12 @@ export default {
:href="restJob.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
variant="confirm"
- data-qa-selector="retry_button"
data-testid="retry-button"
@updateVariablesClicked="$emit('updateVariables')"
/>
<gl-button
v-if="restJob.cancel_path"
- v-gl-tooltip.left
+ v-gl-tooltip.bottom
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
:href="restJob.cancel_path"
@@ -136,7 +158,7 @@ export default {
/>
<gl-button
:aria-label="$options.i18n.toggleSidebar"
- category="tertiary"
+ category="secondary"
class="gl-md-display-none gl-ml-2"
icon="chevron-double-lg-right"
@click="toggleSidebar"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
index 09335476008..ebef3ecaa3f 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
@@ -44,10 +44,14 @@ export default {
this.job.finished_at ||
this.job.erased_at ||
this.job.queued_duration ||
+ this.job.id ||
this.job.runner ||
this.job.coverage,
);
},
+ jobId() {
+ return this.job?.id ? `#${this.job.id}` : '';
+ },
runnerId() {
const { id, short_sha: token, description } = this.job.runner;
@@ -81,8 +85,9 @@ export default {
ERASED: __('Erased'),
QUEUED: __('Queued'),
RUNNER: __('Runner'),
- TAGS: __('Tags:'),
+ TAGS: __('Tags'),
TIMEOUT: __('Timeout'),
+ ID: __('Job ID'),
},
TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
anchor: 'set-a-limit-for-how-long-jobs-can-run',
@@ -108,6 +113,7 @@ export default {
data-testid="job-timeout"
:title="$options.i18n.TIMEOUT"
/>
+ <detail-row v-if="job.id" :value="jobId" :title="$options.i18n.ID" />
<detail-row
v-if="job.runner"
:value="runnerId"
@@ -117,8 +123,8 @@ export default {
<detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
- <span class="font-weight-bold">{{ $options.i18n.TAGS }}</span>
- <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info">{{ tag }}</gl-badge>
+ <span class="font-weight-bold">{{ $options.i18n.TAGS }}:</span>
+ <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info" size="sm">{{ tag }}</gl-badge>
</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index c1f84adf664..7744395734f 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,20 +1,20 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard';
import { keysFor, MR_COPY_SOURCE_BRANCH_NAME } from '~/behaviors/shortcuts/keybindings';
export default {
components: {
- CiIcon,
ClipboardButton,
GlDisclosureDropdown,
GlLink,
GlSprintf,
+ CiBadgeLink,
},
props: {
pipeline: {
@@ -51,11 +51,13 @@ export default {
},
pipelineInfo() {
if (!this.hasRef) {
- return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id}');
- } else if (!this.isTriggeredByMergeRequest) {
- return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{ref}');
- } else if (!this.isMergeRequestPipeline) {
- return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} for %{mrId} with %{source}');
+ return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status}');
+ }
+ if (!this.isTriggeredByMergeRequest) {
+ return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{ref}');
+ }
+ if (!this.isMergeRequestPipeline) {
+ return s__('Job|%{boldStart}Pipeline%{boldEnd} %{id} %{status} for %{mrId} with %{source}');
}
return s__(
@@ -78,7 +80,8 @@ export default {
if (!this.hasRef) {
return;
- } else if (!this.isTriggeredByMergeRequest) {
+ }
+ if (!this.isTriggeredByMergeRequest) {
button = this.$refs['copy-source-ref-link'];
} else {
button = this.$refs['copy-source-branch-link'];
@@ -91,25 +94,30 @@ export default {
</script>
<template>
<div class="dropdown">
- <div class="js-pipeline-info" data-testid="pipeline-info">
- <ci-icon :status="pipeline.details.status" />
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2 js-pipeline-info" data-testid="pipeline-info">
<gl-sprintf :message="pipelineInfo">
<template #bold="{ content }">
- <span class="font-weight-bold">{{ content }}</span>
+ <span class="gl-display-flex gl-font-weight-bold">{{ content }}</span>
</template>
<template #id>
<gl-link
:href="pipeline.path"
- class="js-pipeline-path link-commit"
+ class="js-pipeline-path link-commit gl-text-blue-500!"
data-testid="pipeline-path"
- data-qa-selector="pipeline_path"
>#{{ pipeline.id }}</gl-link
>
</template>
+ <template #status>
+ <ci-badge-link
+ :status="pipeline.details.status"
+ size="sm"
+ data-testid="pipeline-status-link"
+ />
+ </template>
<template #mrId>
<gl-link
:href="pipeline.merge_request.path"
- class="link-commit ref-name"
+ class="link-commit gl-text-blue-500!"
data-testid="mr-link"
>!{{ pipeline.merge_request.iid }}</gl-link
>
@@ -117,7 +125,7 @@ export default {
<template #ref>
<gl-link
:href="pipeline.ref.path"
- class="link-commit ref-name"
+ class="link-commit ref-name gl-mt-1"
data-testid="source-ref-link"
>{{ pipeline.ref.name }}</gl-link
><clipboard-button
@@ -132,7 +140,7 @@ export default {
<template #source>
<gl-link
:href="pipeline.merge_request.source_branch_path"
- class="link-commit ref-name"
+ class="link-commit ref-name gl-mt-1"
data-testid="source-branch-link"
>{{ pipeline.merge_request.source_branch }}</gl-link
><clipboard-button
@@ -147,7 +155,7 @@ export default {
<template #target>
<gl-link
:href="pipeline.merge_request.target_branch_path"
- class="link-commit ref-name"
+ class="link-commit ref-name gl-mt-1"
data-testid="target-branch-link"
>{{ pipeline.merge_request.target_branch }}</gl-link
><clipboard-button
@@ -165,7 +173,7 @@ export default {
:toggle-text="selectedStage"
:items="dropdownItems"
block
- class="gl-mt-3"
+ class="gl-mt-2"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue b/app/assets/javascripts/ci/job_details/components/sidebar/trigger_block.vue
index c9172fe0322..315587a3376 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/trigger_block.vue
@@ -68,7 +68,7 @@ export default {
<template v-if="hasVariables">
<p class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
- <span class="gl-font-weight-bold">{{ __('Trigger variables:') }}</span>
+ <span class="gl-display-flex gl-font-weight-bold">{{ __('Trigger variables') }}</span>
<gl-button
v-if="hasValues"
diff --git a/app/assets/javascripts/jobs/components/job/stuck_block.vue b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
index 1a678ce69a8..8c73f09daea 100644
--- a/app/assets/javascripts/jobs/components/job/stuck_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/stuck_block.vue
@@ -43,7 +43,8 @@ export default {
dataTestId: 'job-stuck-with-tags',
showTags: true,
};
- } else if (this.hasOfflineRunnersForProject) {
+ }
+ if (this.hasOfflineRunnersForProject) {
return {
text: s__(`Job|This job is stuck because the project
doesn't have any runners online assigned to it.`),
diff --git a/app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue b/app/assets/javascripts/ci/job_details/components/unmet_prerequisites_block.vue
index c9747ca9f02..c9747ca9f02 100644
--- a/app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/unmet_prerequisites_block.vue
diff --git a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
index f4a0b10672e..7fb887b2dd4 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_job.fragment.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_variable.fragment.graphql"
fragment BaseCiJob on CiJob {
id
diff --git a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
index 0479df7bc4c..0479df7bc4c 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/fragments/ci_variable.fragment.graphql
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql
index 520deef5136..5d8a7b4c6f6 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_play_with_variables.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
mutation playJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
jobPlay(input: { id: $id, variables: $variables }) {
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
index e35d603ea71..cd66a30ce63 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
jobRetry(input: { id: $id, variables: $variables }) {
diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/ci/job_details/graphql/queries/get_job.query.graphql
index 95e3521091d..a521ec2bb72 100644
--- a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
+++ b/app/assets/javascripts/ci/job_details/graphql/queries/get_job.query.graphql
@@ -1,4 +1,4 @@
-#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql"
+#import "~/ci/job_details/graphql/fragments/ci_job.fragment.graphql"
query getJob($fullPath: ID!, $id: JobID!) {
project(fullPath: $fullPath) {
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/ci/job_details/index.js
index 8cd69f25218..5a1ecf2fff3 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import JobApp from './components/job/job_app.vue';
+import JobApp from './job_app.vue';
import createStore from './store';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index 52030a0f830..5137ebfeaa8 100644
--- a/app/assets/javascripts/jobs/components/job/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -4,25 +4,25 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState, mapActions } from 'vuex';
-import LogTopBar from 'ee_else_ce/jobs/components/job/job_log_controllers.vue';
+import LogTopBar from 'ee_else_ce/ci/job_details/components/job_log_controllers.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { __, sprintf } from '~/locale';
-import CiHeader from '~/vue_shared/components/header_ci_component.vue';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
-import Log from '~/jobs/components/log/log.vue';
-import { MANUAL_STATUS } from '~/jobs/constants';
-import EmptyState from './empty_state.vue';
-import EnvironmentsBlock from './environments_block.vue';
-import ErasedBlock from './erased_block.vue';
-import StuckBlock from './stuck_block.vue';
-import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
-import Sidebar from './sidebar/sidebar.vue';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
+import Log from '~/ci/job_details/components/log/log.vue';
+import { MANUAL_STATUS } from '~/ci/constants';
+import EmptyState from './components/empty_state.vue';
+import EnvironmentsBlock from './components/environments_block.vue';
+import ErasedBlock from './components/erased_block.vue';
+import JobHeader from './components/job_header.vue';
+import StuckBlock from './components/stuck_block.vue';
+import UnmetPrerequisitesBlock from './components/unmet_prerequisites_block.vue';
+import Sidebar from './components/sidebar/sidebar.vue';
export default {
name: 'JobPageApp',
components: {
- CiHeader,
+ JobHeader,
EmptyState,
EnvironmentsBlock,
ErasedBlock,
@@ -33,7 +33,7 @@ export default {
UnmetPrerequisitesBlock,
Sidebar,
GlLoadingIcon,
- SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
+ SharedRunner: () => import('ee_component/ci/runner/components/shared_runner_limit_block.vue'),
GlAlert,
},
directives: {
@@ -129,7 +129,7 @@ export default {
return Boolean(this.job.retry_path);
},
- itemName() {
+ jobName() {
return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
},
},
@@ -225,13 +225,12 @@ export default {
<!-- Header Section -->
<header>
<div class="build-header top-area">
- <ci-header
+ <job-header
:status="job.status"
:time="headerTime"
:user="job.user"
- :has-sidebar-button="true"
:should-render-triggered-label="shouldRenderTriggeredLabel"
- :item-name="itemName"
+ :name="jobName"
@clickedSidebarButton="toggleSidebar"
/>
</div>
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index b348478ccda..33d83689e61 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -12,7 +12,7 @@ import {
scrollUp,
} from '~/lib/utils/scroll_utils';
import { __ } from '~/locale';
-import { reportToSentry } from '../utils';
+import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
export const init = ({ dispatch }, { endpoint, logState, pagePath }) => {
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/ci/job_details/store/getters.js
index a0f9db7409d..a0f9db7409d 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/ci/job_details/store/getters.js
diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/ci/job_details/store/index.js
index b9d76765d8d..b9d76765d8d 100644
--- a/app/assets/javascripts/jobs/store/index.js
+++ b/app/assets/javascripts/ci/job_details/store/index.js
diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js
index 4915a826b84..4915a826b84 100644
--- a/app/assets/javascripts/jobs/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index b7d7006ee61..b7d7006ee61 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/ci/job_details/store/state.js
index dfff65c364d..dfff65c364d 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/ci/job_details/store/state.js
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index bc76901026d..bc76901026d 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
diff --git a/app/assets/javascripts/ci/job_details/utils.js b/app/assets/javascripts/ci/job_details/utils.js
new file mode 100644
index 00000000000..4d06c241b4f
--- /dev/null
+++ b/app/assets/javascripts/ci/job_details/utils.js
@@ -0,0 +1,29 @@
+export const compactJobLog = (jobLog) => {
+ const compactedLog = [];
+
+ jobLog.forEach((obj) => {
+ // push header section line
+ if (obj.line && obj.isHeader) {
+ compactedLog.push(obj.line);
+ }
+
+ // push lines within section header
+ if (obj.lines?.length > 0) {
+ compactedLog.push(...obj.lines);
+ }
+
+ // push lines from plain log
+ if (!obj.lines && obj.content.length > 0) {
+ compactedLog.push(obj);
+ }
+ });
+
+ return compactedLog;
+};
+
+export const filterAnnotations = (annotations, type) => {
+ return [...annotations]
+ .sort((a, b) => a.name.localeCompare(b.name))
+ .flatMap((annotationList) => annotationList.data)
+ .flatMap((annotation) => annotation[type] ?? []);
+};
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
index d97f6f6ff8c..609f2790869 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -7,6 +7,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { reportMessageToSentry } from '~/ci/utils';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import {
@@ -23,13 +24,12 @@ import {
PLAY_JOB_CONFIRMATION_MESSAGE,
RUN_JOB_NOW_HEADER_TITLE,
FILE_TYPE_ARCHIVE,
-} from '../constants';
-import eventHub from '../event_hub';
-import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql';
-import playJobMutation from '../graphql/mutations/job_play.mutation.graphql';
-import retryJobMutation from '../graphql/mutations/job_retry.mutation.graphql';
-import unscheduleJobMutation from '../graphql/mutations/job_unschedule.mutation.graphql';
-import { reportMessageToSentry } from '../../../utils';
+} from '../../constants';
+import eventHub from '../../event_hub';
+import cancelJobMutation from '../../graphql/mutations/job_cancel.mutation.graphql';
+import playJobMutation from '../../graphql/mutations/job_play.mutation.graphql';
+import retryJobMutation from '../../graphql/mutations/job_retry.mutation.graphql';
+import unscheduleJobMutation from '../../graphql/mutations/job_unschedule.mutation.graphql';
export default {
ACTIONS_DOWNLOAD_ARTIFACTS,
diff --git a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
index 11593fa355a..dbf1dfe7a29 100644
--- a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
@@ -27,6 +27,9 @@ export default {
durationFormatted() {
return formatTime(this.duration * 1000);
},
+ hasDurationAndFinishedTime() {
+ return this.finishedTime && this.duration;
+ },
},
};
</script>
@@ -37,7 +40,11 @@ export default {
<gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
{{ durationFormatted }}
</div>
- <div v-if="finishedTime" data-testid="job-finished-time">
+ <div
+ v-if="finishedTime"
+ :class="{ 'gl-mt-2': hasDurationAndFinishedTime }"
+ data-testid="job-finished-time"
+ >
<gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
<time-ago-tooltip :time="finishedTime" />
</div>
diff --git a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
index 27d286fc766..b435eb283fd 100644
--- a/app/assets/javascripts/jobs/components/table/cells/job_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -71,7 +71,7 @@ export default {
<template>
<div>
- <div class="gl-text-truncate gl-mb-2">
+ <div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
<gl-link
v-if="canReadJob"
class="gl-text-blue-600!"
@@ -96,7 +96,7 @@ export default {
>
<div
v-if="jobRef"
- class="gl-px-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
+ class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
>
<gl-icon
v-if="createdByTag"
@@ -114,7 +114,7 @@ export default {
</div>
<span v-else>{{ __('none') }}</span>
- <div class="gl-ml-2 gl-rounded-base gl-px-2 gl-bg-gray-50">
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
<gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
<gl-link
class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
diff --git a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
index 1a6d1a341b0..18d68ee8a29 100644
--- a/app/assets/javascripts/jobs/components/table/cells/pipeline_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -36,12 +36,16 @@ export default {
<template>
<div>
- <div class="gl-text-truncate">
- <gl-link class="gl-text-gray-500!" :href="pipelinePath" data-testid="pipeline-id">
+ <div class="gl-p-3 gl-mt-n3">
+ <gl-link
+ class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
+ :href="pipelinePath"
+ data-testid="pipeline-id"
+ >
{{ pipelineId }}
</gl-link>
</div>
- <div>
+ <div class="gl-font-sm gl-text-secondary gl-mt-n2">
<span>{{ __('created by') }}</span>
<gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
<gl-avatar :src="pipelineUserAvatar" :size="16" />
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
index 84479ec421e..23100a3f3db 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -2,13 +2,13 @@
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import ProjectCell from '~/pages/admin/jobs/components/table/cell/project_cell.vue';
-import RunnerCell from '~/pages/admin/jobs/components/table/cells/runner_cell.vue';
-import ActionsCell from './cells/actions_cell.vue';
-import DurationCell from './cells/duration_cell.vue';
-import JobCell from './cells/job_cell.vue';
-import PipelineCell from './cells/pipeline_cell.vue';
-import { DEFAULT_FIELDS } from './constants';
+import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
+import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
+import { DEFAULT_FIELDS } from '../constants';
+import ActionsCell from './job_cells/actions_cell.vue';
+import DurationCell from './job_cells/duration_cell.vue';
+import JobCell from './job_cells/job_cell.vue';
+import PipelineCell from './job_cells/pipeline_cell.vue';
export default {
i18n: {
@@ -85,7 +85,7 @@ export default {
<template #cell(stage)="{ item }">
<div class="gl-text-truncate">
- <span data-testid="job-stage-name">{{ item.stage.name }}</span>
+ <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
index fcdd52b719c..d2cd27be034 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_empty_state.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
@@ -31,5 +31,6 @@ export default {
:svg-path="emptyStateSvgPath"
:primary-button-link="pipelineEditorPath"
:primary-button-text="$options.i18n.buttonText"
+ data-testid="jobs-empty-state"
/>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue
index 797facb1eb8..b753195da9a 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue
@@ -1,7 +1,7 @@
<script>
import { GlBadge, GlTab, GlTabs, GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CancelJobs from '~/pages/admin/jobs/components/cancel_jobs.vue';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
import { limitedCounterWithDelimiter } from '~/lib/utils/text_utility';
export default {
@@ -63,7 +63,7 @@ export default {
<template>
<div class="gl-display-flex align-items-lg-center">
- <gl-tabs content-class="gl-py-0">
+ <gl-tabs content-class="gl-py-0" class="gl-w-full">
<gl-tab
v-for="tab in tabs"
:key="tab.text"
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/ci/jobs_page/constants.js
index 1b572e60c58..1b572e60c58 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/ci/jobs_page/constants.js
diff --git a/app/assets/javascripts/pipelines/event_hub.js b/app/assets/javascripts/ci/jobs_page/event_hub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/pipelines/event_hub.js
+++ b/app/assets/javascripts/ci/jobs_page/event_hub.js
diff --git a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/ci/jobs_page/graphql/cache_config.js
index 5390c023da4..5390c023da4 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js
+++ b/app/assets/javascripts/ci/jobs_page/graphql/cache_config.js
diff --git a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql b/app/assets/javascripts/ci/jobs_page/graphql/fragments/job.fragment.graphql
index 3038216fdfc..3038216fdfc 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/fragments/job.fragment.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql
index 20935514d51..20935514d51 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_cancel.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_play.mutation.graphql
index c94b045ac40..c94b045ac40 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_play.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
index 6e51f9a20fa..6e51f9a20fa 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_retry.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql
index 8be8c42f3c3..8be8c42f3c3 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/mutations/job_unschedule.mutation.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
index 69719011079..69719011079 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql
index a4e02ae721a..a4e02ae721a 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs_count.query.graphql
diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/ci/jobs_page/index.js
index 88da1169e01..7e99157289b 100644
--- a/app/assets/javascripts/jobs/components/table/index.js
+++ b/app/assets/javascripts/ci/jobs_page/index.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
+import JobsTableApp from '~/ci/jobs_page/jobs_page_app.vue';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import cacheConfig from './graphql/cache_config';
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
index 09fa006cb88..03e0f2dadc8 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/ci/jobs_page/jobs_page_app.vue
@@ -3,14 +3,14 @@ import { GlAlert, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
-import JobsSkeletonLoader from '~/pages/admin/jobs/components/jobs_skeleton_loader.vue';
-import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
-import { validateQueryString } from '../filtered_search/utils';
+import JobsSkeletonLoader from '~/ci/admin/jobs_table/components/jobs_skeleton_loader.vue';
+import JobsFilteredSearch from '~/ci/common/private/jobs_filtered_search/app.vue';
+import { validateQueryString } from '~/ci/common/private/jobs_filtered_search/utils';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import GetJobsCount from './graphql/queries/get_jobs_count.query.graphql';
-import JobsTable from './jobs_table.vue';
-import JobsTableEmptyState from './jobs_table_empty_state.vue';
-import JobsTableTabs from './jobs_table_tabs.vue';
+import JobsTable from './components/jobs_table.vue';
+import JobsTableEmptyState from './components/jobs_table_empty_state.vue';
+import JobsTableTabs from './components/jobs_table_tabs.vue';
import { RAW_TEXT_WARNING } from './constants';
export default {
diff --git a/app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue b/app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue
new file mode 100644
index 00000000000..ee911d716e4
--- /dev/null
+++ b/app/assets/javascripts/ci/merge_requests/components/pipelines_table_wrapper.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
+import { graphqlEtagMergeRequestPipelines } from '~/ci/pipeline_details/utils';
+import getMergeRequestPipelines from '../graphql/queries/get_merge_request_pipelines.query.graphql';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ inject: ['graphqlPath', 'mergeRequestId', 'targetProjectFullPath'],
+ data() {
+ return {
+ pipelines: [],
+ };
+ },
+ apollo: {
+ pipelines: {
+ query: getMergeRequestPipelines,
+ context() {
+ return getQueryHeaders(this.graphqlResourceEtag);
+ },
+ pollInterval: 10000,
+ variables() {
+ return {
+ fullPath: this.targetProjectFullPath,
+ mergeRequestIid: String(this.mergeRequestId),
+ };
+ },
+ update(data) {
+ return data?.project?.mergeRequest?.pipelines?.nodes || [];
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.fetchError });
+ },
+ },
+ },
+ computed: {
+ graphqlResourceEtag() {
+ return graphqlEtagMergeRequestPipelines(this.graphqlPath, this.mergeRequestId);
+ },
+ isLoading() {
+ return this.$apollo.queries.pipelines.loading;
+ },
+ },
+ i18n: {
+ fetchError: __("There was an error fetching this merge request's pipelines."),
+ },
+};
+</script>
+<template>
+ <div class="gl-mt-3">
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <ul v-else>
+ <li v-for="pipeline in pipelines" :key="pipeline.id">{{ pipeline.path }}</li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
index 022d461dbec..022d461dbec 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql
diff --git a/app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql b/app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql
new file mode 100644
index 00000000000..8c235032e6c
--- /dev/null
+++ b/app/assets/javascripts/ci/merge_requests/graphql/queries/get_merge_request_pipelines.query.graphql
@@ -0,0 +1,16 @@
+query getMergeRequestPipelines($mergeRequestIid: String!, $fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ mergeRequest(iid: $mergeRequestIid) {
+ id
+ pipelines {
+ count
+ nodes {
+ id
+ iid
+ path
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js b/app/assets/javascripts/ci/mixins/delayed_job_mixin.js
index 7b17dc7f693..7b17dc7f693 100644
--- a/app/assets/javascripts/jobs/mixins/delayed_job_mixin.js
+++ b/app/assets/javascripts/ci/mixins/delayed_job_mixin.js
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index 93ca3738ff0..bf312e66144 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -1,22 +1,11 @@
-import { s__, __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
-export const FILTER_PIPELINES_SEARCH_DELAY = 200;
-export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
-export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
-export const ICONS = {
- TAG: 'tag',
- MR: 'git-merge',
- BRANCH: 'branch',
- RETRY: 'retry',
- SUCCESS: 'success',
-};
-
export const TestStatus = {
FAILED: 'failed',
SKIPPED: 'skipped',
@@ -25,13 +14,6 @@ export const TestStatus = {
UNKNOWN: 'unknown',
};
-export const FETCH_AUTHOR_ERROR_MESSAGE = __('There was a problem fetching project users.');
-export const FETCH_BRANCH_ERROR_MESSAGE = __('There was a problem fetching project branches.');
-export const FETCH_TAG_ERROR_MESSAGE = __('There was a problem fetching project tags.');
-export const RAW_TEXT_WARNING = s__(
- 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.',
-);
-
/* Error constants shared across graphs */
export const DEFAULT = 'default';
export const DELETE_FAILURE = 'delete_pipeline_failure';
@@ -64,25 +46,8 @@ export const validPipelineTabNames = [
codeQualityTabName,
];
-// Constants for the ID and IID selection dropdown
-export const PipelineKeyOptions = [
- {
- text: __('Show Pipeline ID'),
- label: __('Pipeline ID'),
- value: 'id',
- },
- {
- text: __('Show Pipeline IID'),
- label: __('Pipeline IID'),
- value: 'iid',
- },
-];
-
export const TOAST_MESSAGE = s__('Pipeline|Creating pipeline.');
-export const BUTTON_TOOLTIP_RETRY = __('Retry all failed or cancelled jobs');
-export const BUTTON_TOOLTIP_CANCEL = __('Cancel the running pipeline');
-
export const DEFAULT_FIELDS = [
{
key: 'name',
@@ -107,14 +72,6 @@ export const DEFAULT_FIELDS = [
},
];
-export const TRACKING_CATEGORIES = {
- table: 'pipelines_table_component',
- tabs: 'pipelines_filter_tabs',
- search: 'pipelines_filtered_search',
- failed: 'pipeline_failed_jobs_tab',
- tests: 'pipeline_tests_tab',
-};
-
// Pipeline Mini Graph
export const PIPELINE_MINI_GRAPH_POLL_INTERVAL = 5000;
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue
index a1500166cdc..a1500166cdc 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_annotations.vue
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue
index 7646c11773c..6e975d55a7f 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/components/dag_graph.vue
@@ -1,10 +1,10 @@
<script>
import * as d3 from 'd3';
import { uniqueId } from 'lodash';
+import { getMaxNodes, removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
import { PARSE_FAILURE } from '../../constants';
-import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
-import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
+import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '../constants';
+import { calculateClip, createLinkPath, createSankey, labelPosition } from '../utils/drawing_utils';
import {
currentIsLive,
getLiveLinksAsDict,
@@ -12,7 +12,7 @@ import {
restoreLinks,
toggleLinkHighlight,
togglePathHighlights,
-} from './interactions';
+} from '../utils/interactions';
export default {
viewOptions: {
diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/ci/pipeline_details/dag/constants.js
index cd89055737f..cd89055737f 100644
--- a/app/assets/javascripts/pipelines/components/dag/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/dag/constants.js
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
index afb5aa05098..5415340c956 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/ci/pipeline_details/dag/dag.vue
@@ -4,12 +4,17 @@ import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { __ } from '~/locale';
-import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
-import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
-import { parseData } from '../parsing_utils';
+import {
+ DEFAULT,
+ PARSE_FAILURE,
+ LOAD_FAILURE,
+ UNSUPPORTED_DATA,
+} from '~/ci/pipeline_details/constants';
+import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
+import getDagVisData from './graphql/queries/get_dag_vis_data.query.graphql';
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import DagAnnotations from './dag_annotations.vue';
-import DagGraph from './dag_graph.vue';
+import DagAnnotations from './components/dag_annotations.vue';
+import DagGraph from './components/dag_graph.vue';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql
index 2a0b13dd0cc..2a0b13dd0cc 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_dag_vis_data.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/dag/graphql/queries/get_dag_vis_data.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js
index 3cd09d57ffb..3cd09d57ffb 100644
--- a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/dag/utils/drawing_utils.js
diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js
index 69f36feeee4..d2b7b7f9069 100644
--- a/app/assets/javascripts/pipelines/components/dag/interactions.js
+++ b/app/assets/javascripts/ci/pipeline_details/dag/utils/interactions.js
@@ -1,5 +1,5 @@
import * as d3 from 'd3';
-import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from './constants';
+import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from '../constants';
export const highlightIn = 1;
export const highlightOut = 0.2;
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/api.js b/app/assets/javascripts/ci/pipeline_details/graph/api_utils.js
index 0fe7d9ffda3..f9f47d1ea15 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/api.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/api_utils.js
@@ -1,5 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import { reportToSentry } from '../../utils';
+import { reportToSentry } from '~/ci/utils';
export const reportPerformance = (path, stats) => {
// FIXME: https://gitlab.com/gitlab-org/gitlab/-/issues/330245
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
index 49df71beeec..f098d790736 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_component.vue
@@ -1,15 +1,15 @@
<script>
-import { reportToSentry } from '../../utils';
-import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
-import LinksLayer from '../graph_shared/links_layer.vue';
+import { reportToSentry } from '~/ci/utils';
import {
generateColumnsFromLayersListMemoized,
keepLatestDownstreamPipelines,
-} from '../parsing_utils';
-import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants';
+} from '~/ci/pipeline_details/utils/parsing_utils';
+import LinksLayer from '../../../common/private/job_links_layer.vue';
+import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from '../constants';
+import { validateConfigPaths } from '../utils';
+import LinkedGraphWrapper from './linked_graph_wrapper.vue';
import LinkedPipelinesColumn from './linked_pipelines_column.vue';
import StageColumnComponent from './stage_column_component.vue';
-import { validateConfigPaths } from './utils';
export default {
name: 'PipelineGraph',
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
index 73143c981ed..fb7dcb300f1 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/graph_view_selector.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { STAGE_VIEW, LAYER_VIEW } from './constants';
+import { STAGE_VIEW, LAYER_VIEW } from '../constants';
export default {
name: 'GraphViewSelector',
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
index d4852224df5..7538ad87af8 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_group_dropdown.vue
@@ -1,6 +1,6 @@
<script>
-import { reportToSentry } from '../../utils';
-import { JOB_DROPDOWN, SINGLE_JOB } from './constants';
+import { reportToSentry } from '~/ci/utils';
+import { JOB_DROPDOWN, SINGLE_JOB } from '../constants';
import JobItem from './job_item.vue';
/**
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index 22895a31082..4298052d1c0 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -1,14 +1,14 @@
<script>
import { GlBadge, GlForm, GlFormCheckbox, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import { reportToSentry } from '~/ci/utils';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from './constants';
+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';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
index fb2280d971a..fb2280d971a 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_graph_wrapper.vue
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index d8b843bdfb0..d6adaf78da4 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -11,11 +11,11 @@ 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 '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
+import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
+import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { reportToSentry } from '../../utils';
-import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
+import { reportToSentry } from '~/ci/utils';
+import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
export default {
directives: {
@@ -72,7 +72,8 @@ export default {
method: this.cancelPipeline,
ariaLabel: __('Cancel downstream pipeline'),
};
- } else if (this.isRetryable) {
+ }
+ if (this.isRetryable) {
return {
icon: 'retry',
method: this.retryPipeline,
@@ -141,7 +142,8 @@ export default {
label() {
if (this.parentPipeline) {
return __('Parent');
- } else if (this.childPipeline) {
+ }
+ if (this.childPipeline) {
return __('Child');
}
return __('Multi-project');
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
index 02e426064c9..2de7e43c9b1 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipelines_column.vue
@@ -1,9 +1,8 @@
<script>
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
+import { reportToSentry } from '~/ci/utils';
import { LOAD_FAILURE } from '../../constants';
-import { reportToSentry } from '../../utils';
-import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
-import LinkedPipeline from './linked_pipeline.vue';
+import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from '../constants';
import {
calculatePipelineLayersInfo,
getQueryHeaders,
@@ -11,7 +10,8 @@ import {
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,
-} from './utils';
+} from '../utils';
+import LinkedPipeline from './linked_pipeline.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/links_inner.vue
index 1189c2ebad8..09285525c38 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/links_inner.vue
@@ -1,9 +1,10 @@
<script>
import { isEmpty } from 'lodash';
+import { STAGE_VIEW } from '~/ci/pipeline_details/graph/constants';
+import { createJobsHash, generateJobNeedsDict } from '~/ci/pipeline_details/utils';
+import { reportToSentry } from '~/ci/utils';
import { DRAW_FAILURE } from '../../constants';
-import { createJobsHash, generateJobNeedsDict, reportToSentry } from '../../utils';
-import { STAGE_VIEW } from '../graph/constants';
-import { generateLinksData } from './drawing_utils';
+import { generateLinksData } from '../../utils/drawing_utils';
export default {
name: 'LinksInner',
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
index bcd7705669e..bcd7705669e 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/root_graph_layout.vue
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
index ffd0fec2ca8..1401bdba5ca 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/stage_column_component.vue
@@ -1,9 +1,9 @@
<script>
import { escape, isEmpty } from 'lodash';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportToSentry } from '../../utils';
-import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
-import ActionComponent from '../jobs_shared/action_component.vue';
+import { reportToSentry } from '~/ci/utils';
+import RootGraphLayout from './root_graph_layout.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
@@ -12,7 +12,7 @@ export default {
ActionComponent,
JobGroupDropdown,
JobItem,
- MainGraphWrapper,
+ RootGraphLayout,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -135,7 +135,7 @@ export default {
};
</script>
<template>
- <main-graph-wrapper :class="columnSpacingClass" data-testid="stage-column">
+ <root-graph-layout :class="columnSpacingClass" data-testid="stage-column">
<template #stages>
<div
data-testid="stage-column-title"
@@ -192,5 +192,5 @@ export default {
</div>
</div>
</template>
- </main-graph-wrapper>
+ </root-graph-layout>
</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/ci/pipeline_details/graph/constants.js
index e650a48bc2a..e650a48bc2a 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/constants.js
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
index b2cef7c37b9..bd7325f7925 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graph_component_wrapper.vue
@@ -4,10 +4,10 @@ import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.qu
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
-import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
-import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
-import { reportToSentry, reportMessageToSentry } from '../../utils';
+import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '~/ci/pipeline_details/constants';
+import getPipelineQuery from '~/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql';
+import { reportToSentry, reportMessageToSentry } from '~/ci/utils';
+import DismissPipelineGraphCallout from './graphql/mutations/dismiss_pipeline_notification.graphql';
import {
ACTION_FAILURE,
IID_FAILURE,
@@ -16,8 +16,8 @@ import {
STAGE_VIEW,
VIEW_TYPE_KEY,
} from './constants';
-import PipelineGraph from './graph_component.vue';
-import GraphViewSelector from './graph_view_selector.vue';
+import PipelineGraph from './components/graph_component.vue';
+import GraphViewSelector from './components/graph_view_selector.vue';
import {
calculatePipelineLayersInfo,
getQueryHeaders,
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql b/app/assets/javascripts/ci/pipeline_details/graph/graphql/mutations/dismiss_pipeline_notification.graphql
index e8af1db9592..e8af1db9592 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/dismiss_pipeline_notification.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graph/graphql/mutations/dismiss_pipeline_notification.graphql
diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/ci/pipeline_details/graph/perf_utils.js
index 3737a209f5c..511dcbe6889 100644
--- a/app/assets/javascripts/pipelines/components/graph/perf_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/perf_utils.js
@@ -8,7 +8,7 @@ import {
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
-import { reportPerformance } from '../graph_shared/api';
+import { reportPerformance } from './api_utils';
export const beginPerfMeasure = () => {
performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/ci/pipeline_details/graph/utils.js
index c888c8a5537..9a8d6440d4d 100644
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/graph/utils.js
@@ -1,8 +1,9 @@
import { isEmpty } from 'lodash';
import { getIdFromGraphQLId, etagQueryHeaders } from '~/graphql_shared/utils';
-import { reportToSentry } from '../../utils';
-import { listByLayers } from '../parsing_utils';
-import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
+import { reportToSentry } from '~/ci/utils';
+
+import { listByLayers } from '~/ci/pipeline_details/utils/parsing_utils';
+import { unwrapStagesWithNeedsAndLookup } from '~/ci/pipeline_details/utils/unwrapping_utils';
import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
export { toggleQueryPollingByVisibility } from '~/graphql_shared/utils';
diff --git a/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql
index f93908aeb04..f93908aeb04 100644
--- a/app/assets/javascripts/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql
index 9afb474cb17..9afb474cb17 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/delete_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql
index a52cecafcaf..a52cecafcaf 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/delete_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/delete_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql
index 2b3b0822653..2b3b0822653 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/retry_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/provider.js b/app/assets/javascripts/ci/pipeline_details/graphql/provider.js
index ef96b443da8..ef96b443da8 100644
--- a/app/assets/javascripts/pipelines/graphql/provider.js
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/provider.js
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql
index 9257cc7de7b..9257cc7de7b 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_linked_pipelines.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
index eb5643126a2..eb5643126a2 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index c53321f82bd..3a6a655bfa6 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -11,6 +11,7 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/ci/constants';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { __, s__, sprintf, formatNumber } from '~/locale';
@@ -19,19 +20,12 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.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,
- BUTTON_TOOLTIP_RETRY,
- BUTTON_TOOLTIP_CANCEL,
-} from '../constants';
+import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
-import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
-import { getQueryHeaders } from './graph/utils';
+import { getQueryHeaders } from '../graph/utils';
+import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
const DELETE_MODAL_ID = 'pipeline-delete-modal';
const POLL_INTERVAL = 10000;
@@ -63,7 +57,7 @@ export default {
},
i18n: {
scheduleBadgeText: s__('Pipelines|Scheduled'),
- scheduleBadgeTooltip: __('This pipeline was triggered by a schedule'),
+ scheduleBadgeTooltip: __('This pipeline was created by a schedule'),
childBadgeText: s__('Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})'),
childBadgeTooltip: __('This is a child pipeline within the parent pipeline'),
latestBadgeText: s__('Pipelines|latest'),
@@ -272,7 +266,7 @@ export default {
});
},
triggeredText() {
- return sprintf(__('triggered pipeline for commit %{linkStart}%{shortId}%{linkEnd}'), {
+ return sprintf(__('created pipeline for commit %{linkStart}%{shortId}%{linkEnd}'), {
shortId: this.shortId,
});
},
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index f84ae13180d..4752fbb3e96 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -6,8 +6,9 @@ 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 RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
-import { DEFAULT_FIELDS, TRACKING_CATEGORIES } from '../../constants';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
+import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
+import { DEFAULT_FIELDS } from '../../constants';
export default {
fields: DEFAULT_FIELDS,
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/ci/pipeline_details/jobs/failed_jobs_app.vue
index c24862f828b..b946a40e590 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/failed_jobs_app.vue
@@ -2,8 +2,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
-import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
-import FailedJobsTable from './failed_jobs_table.vue';
+import GetFailedJobsQuery from './graphql/queries/get_failed_jobs.query.graphql';
+import FailedJobsTable from './components/failed_jobs_table.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
index 1955cc9b0ac..1955cc9b0ac 100644
--- a/app/assets/javascripts/pipelines/graphql/mutations/retry_failed_job.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/mutations/retry_failed_job.mutation.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql
index c1f994ece24..c1f994ece24 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_failed_jobs.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql
index b0f875160d4..b0f875160d4 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_jobs.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/graphql/queries/get_pipeline_jobs.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/ci/pipeline_details/jobs/jobs_app.vue
index 61748860983..81b6152347d 100644
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/jobs_app.vue
@@ -3,10 +3,10 @@ import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab
import produce from 'immer';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import eventHub from '~/jobs/components/table/event_hub';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import { JOBS_TAB_FIELDS } from '~/jobs/components/table/constants';
-import getPipelineJobs from '../../graphql/queries/get_pipeline_jobs.query.graphql';
+import eventHub from '~/ci/jobs_page/event_hub';
+import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import { JOBS_TAB_FIELDS } from '~/ci/jobs_page/constants';
+import getPipelineJobs from './graphql/queries/get_pipeline_jobs.query.graphql';
export default {
fields: JOBS_TAB_FIELDS,
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
index 481953608e9..53f755fda37 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
@@ -1,13 +1,13 @@
import Visibility from 'visibilityjs';
import { createAlert } from '~/alert';
+import eventHub from '~/ci/event_hub';
import { helpPagePath } from '~/helpers/help_page_helper';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
-import { validateParams } from '~/pipelines/utils';
+import { validateParams } from '~/ci/pipeline_details/utils';
import { CANCEL_REQUEST, TOAST_MESSAGE } from '../constants';
-import eventHub from '../event_hub';
export default {
data() {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_bundle.js
index f9c027539f2..da09852a7f4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_bundle.js
@@ -33,9 +33,15 @@ export default async function initPipelineDetailsBundle() {
if (tabsEl) {
const { dataset } = tabsEl;
- const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
+ const dismissalDescriptions = JSON.parse(dataset.dismissalDescriptions || '{}');
+ const { createAppOptions } = await import('ee_else_ce/ci/pipeline_details/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
- const { routes } = await import('ee_else_ce/pipelines/routes');
+ const { routes } = await import('ee_else_ce/ci/pipeline_details/routes');
+
+ const securityRoute = routes.find((route) => route.path === '/security');
+ if (securityRoute) {
+ securityRoute.props = { dismissalDescriptions };
+ }
const router = new VueRouter({
mode: 'history',
diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index c79aaef23e8..067ec3f305e 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
-import PipelineDetailsHeader from './components/pipeline_details_header.vue';
+import PipelineDetailsHeader from './header/pipeline_details_header.vue';
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
index c3be487caae..c3be487caae 100644
--- a/app/assets/javascripts/pipelines/pipeline_shared_client.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_shared_client.js
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js
index 00a1810926c..0ca9a68e70d 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_tabs.js
@@ -4,10 +4,11 @@ import VueRouter from 'vue-router';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import { GlToast } from '@gitlab/ui';
-import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
+import PipelineTabs from 'ee_else_ce/ci/pipeline_details/tabs/pipeline_tabs.vue';
+import { reportToSentry } from '~/ci/utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import createTestReportsStore from './stores/test_reports';
-import { getPipelineDefaultTab, reportToSentry } from './utils';
+import { getPipelineDefaultTab } from './utils';
Vue.use(GlToast);
Vue.use(VueApollo);
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index 20fd0915e28..d38397e7479 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -10,7 +10,7 @@ import {
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
-import Pipelines from './components/pipelines_list/pipelines.vue';
+import Pipelines from '~/ci/pipelines_page/pipelines.vue';
import PipelinesStore from './stores/pipelines_store';
Vue.use(Translate);
diff --git a/app/assets/javascripts/pipelines/routes.js b/app/assets/javascripts/ci/pipeline_details/routes.js
index 0e1414ec390..84207f3ab0c 100644
--- a/app/assets/javascripts/pipelines/routes.js
+++ b/app/assets/javascripts/ci/pipeline_details/routes.js
@@ -1,8 +1,8 @@
-import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
-import Dag from './components/dag/dag.vue';
-import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
-import JobsApp from './components/jobs/jobs_app.vue';
-import TestReports from './components/test_reports/test_reports.vue';
+import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
+import Dag from './dag/dag.vue';
+import FailedJobsApp from './jobs/failed_jobs_app.vue';
+import JobsApp from './jobs/jobs_app.vue';
+import TestReports from './test_reports/test_reports.vue';
import {
pipelineTabName,
needsTabName,
diff --git a/app/assets/javascripts/pipelines/stores/pipelines_store.js b/app/assets/javascripts/ci/pipeline_details/stores/pipelines_store.js
index 765441560d8..765441560d8 100644
--- a/app/assets/javascripts/pipelines/stores/pipelines_store.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/pipelines_store.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/actions.js
index 1b51bb804d0..1b51bb804d0 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/actions.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/constants.js
index 83d14e1a109..83d14e1a109 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/constants.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/getters.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/getters.js
index e6a88bb4175..e6a88bb4175 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/getters.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/getters.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/index.js
index f45a53f47b7..f45a53f47b7 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/index.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/index.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutation_types.js
index 7651a2f4327..7651a2f4327 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutation_types.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutations.js
index 466574157f5..466574157f5 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/mutations.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/state.js
index 3ec9418c14e..3ec9418c14e 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/state.js
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/utils.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
index 6b616601bc5..6b616601bc5 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/ci/pipeline_details/tabs/pipeline_tabs.vue
index 35dde6379dd..9783a9b5937 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_details/tabs/pipeline_tabs.vue
@@ -1,5 +1,6 @@
<script>
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import {
@@ -8,7 +9,6 @@ import {
needsTabName,
pipelineTabName,
testReportTabName,
- TRACKING_CATEGORIES,
} from '../constants';
export default {
diff --git a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue
index 3e7827dc416..055b6742ae1 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/empty_state.vue
@@ -54,6 +54,7 @@ export default {
:title="emptyStateText.title"
:description="emptyStateText.description"
:svg-path="emptyStateImagePath"
+ :svg-height="150"
:primary-button-link="testReportDocPath"
:primary-button-text="emptyStateText.button"
/>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue
index 10db3e1c56b..3e6faa6b346 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_case_details.vue
@@ -29,6 +29,11 @@ export default {
return {};
},
},
+ visible: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
failureHistoryMessage() {
@@ -77,6 +82,8 @@ export default {
:modal-id="modalId"
:title="testCase.classname"
:action-primary="$options.modalCloseButton"
+ :visible="visible"
+ @hidden="$emit('hidden')"
>
<div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
<strong class="gl-text-right col-sm-3">{{ $options.text.name }}</strong>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
index a7737d33285..a7737d33285 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_suite_table.vue
index d8af926a796..d8af926a796 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_suite_table.vue
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary.vue
index 6b723ad5481..f6090678ca4 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlProgressBar } from '@gitlab/ui';
import { __ } from '~/locale';
-import { formattedTime } from '../../stores/test_reports/utils';
+import { formattedTime } from '../stores/test_reports/utils';
export default {
name: 'TestSummary',
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary_table.vue
index 9141947ea04..9141947ea04 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_summary_table.vue
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js
index d6d9ea94c13..d6d9ea94c13 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/drawing_utils.js
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/ci/pipeline_details/utils/index.js
index 38be5becfb8..9109342707e 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/index.js
@@ -1,4 +1,3 @@
-import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import {
@@ -6,7 +5,7 @@ import {
SUPPORTED_FILTER_PARAMETERS,
validPipelineTabNames,
pipelineTabName,
-} from './constants';
+} from '../constants';
/*
The following functions are the main engine in transforming the data as
received from the endpoint into the format the d3 graph expects.
@@ -128,22 +127,6 @@ export const generateJobNeedsDict = (jobs = {}) => {
}, {});
};
-export const reportToSentry = (component, failureType) => {
- Sentry.withScope((scope) => {
- scope.setTag('component', component);
- Sentry.captureException(failureType);
- });
-};
-
-export const reportMessageToSentry = (component, message, context) => {
- Sentry.withScope((scope) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- scope.setContext('Vue data', context);
- scope.setTag('component', component);
- Sentry.captureMessage(message);
- });
-};
-
export const getPipelineDefaultTab = (url) => {
const strippedUrl = parseUrlPathname(url);
const regexp = /\w*$/;
@@ -154,3 +137,11 @@ export const getPipelineDefaultTab = (url) => {
return null;
};
+
+export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
+ return `${graphqlPath}pipelines/id/${pipelineId}`;
+};
+
+export const graphqlEtagMergeRequestPipelines = (graphqlPath, mergeRequestId) => {
+ return `${graphqlPath}merge_requests/id/${mergeRequestId}`;
+};
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js
index e158f8809b5..0a2a6d16498 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/parsing_utils.js
@@ -1,7 +1,7 @@
import { memoize } from 'lodash';
-import { createNodeDict } from '../utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
-import { createSankey } from './dag/drawing_utils';
+import { createSankey } from '../dag/utils/drawing_utils';
+import { createNodeDict } from './index';
/*
A peformant alternative to lodash's isEqual. Because findIndex always finds
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/ci/pipeline_details/utils/unwrapping_utils.js
index d42a11c3aba..7ac813bd527 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/utils/unwrapping_utils.js
@@ -1,4 +1,4 @@
-import { reportToSentry } from '../utils';
+import { reportToSentry } from '~/ci/utils';
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
const unwrapGroups = (stages) => {
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
index 3fe9103c2b3..baf3dbfa090 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
@@ -108,7 +108,6 @@ export default {
<gl-form-group
id="commit-group"
:label="$options.i18n.commitMessage"
- label-cols-sm="2"
label-for="commit-message"
>
<gl-form-textarea
@@ -122,7 +121,6 @@ export default {
<gl-form-group
id="source-branch-group"
:label="$options.i18n.sourceBranch"
- label-cols-sm="2"
label-for="source-branch-field"
>
<gl-form-input
@@ -130,13 +128,12 @@ export default {
v-model="sourceBranch"
class="gl-font-monospace!"
required
- data-qa-selector="source_branch_field"
+ data-testid="source-branch-field"
/>
<gl-form-checkbox
v-if="!isCurrentBranchSourceBranch"
v-model="openMergeRequest"
data-testid="new-mr-checkbox"
- data-qa-selector="new_mr_checkbox"
class="gl-mt-3"
>
<gl-sprintf :message="$options.i18n.startMergeRequest">
@@ -152,7 +149,7 @@ export default {
class="js-no-auto-disable gl-mr-3"
category="primary"
variant="confirm"
- data-qa-selector="commit_changes_button"
+ data-testid="commit-changes-button"
:disabled="isSubmitDisabled"
:loading="isSaving"
>
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 bc0cad75c60..8f4d566e7e6 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
@@ -89,7 +89,6 @@ export default {
icon="external-link"
target="_blank"
data-testid="template-repo-link"
- data-qa-selector="template_repo_link"
@click="trackTemplateBrowsing"
>
{{ $options.i18n.browseTemplates }}
@@ -98,7 +97,6 @@ export default {
icon="information-o"
size="small"
data-testid="drawer-toggle"
- data-qa-selector="drawer_toggle"
@click="toggleHelpDrawer"
>
{{ $options.i18n.help }}
@@ -107,7 +105,6 @@ export default {
v-if="glFeatures.ciJobAssistantDrawer"
icon="bulb"
size="small"
- data-qa-selector="job_assistant_drawer_toggle"
@click="toggleJobAssistantDrawer"
>
{{ $options.i18n.jobAssistant }}
@@ -117,7 +114,6 @@ export default {
icon="bulb"
size="small"
data-testid="ai-assistant-drawer-toggle"
- data-qa-selector="ai_assistant_drawer_toggle"
@click="toggleAiAssistantDrawer"
>
{{ $options.i18n.aiAssistant }}
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 a410e4c933c..221a45d4d9a 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
@@ -218,7 +218,6 @@ export default {
:text="currentBranch"
:disabled="!enableBranchSwitcher"
icon="branch"
- data-qa-selector="branch_selector_button"
data-testid="branch-selector"
>
<gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index 7368d1a3a91..20b42e26f08 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -52,7 +52,7 @@ export default {
};
</script>
<template>
- <div class="gl-mb-4 gl-display-flex gl-flex-wrap gl-gap-3">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3 gl-mb-4">
<gl-button
v-if="showFileTreeToggle"
id="file-tree-toggle"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/ci/pipeline_editor/components/graph/job_pill.vue
index 3f1d7255a2b..3f1d7255a2b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/graph/job_pill.vue
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/graph/pipeline_graph.vue
index 8daf85e2b2e..eb906cfc486 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/graph/pipeline_graph.vue
@@ -1,8 +1,8 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
-import { DRAW_FAILURE, DEFAULT } from '../../constants';
-import LinksLayer from '../graph_shared/links_layer.vue';
+import { DRAW_FAILURE, DEFAULT } from '~/ci/pipeline_details/constants';
+import LinksLayer from '~/ci/common/private/job_links_layer.vue';
import JobPill from './job_pill.vue';
import StageName from './stage_name.vue';
@@ -132,7 +132,6 @@ export default {
:ref="$options.CONTAINER_REF"
class="gl-bg-gray-10 gl-overflow-auto"
data-testid="graph-container"
- data-qa-selector="pipeline_graph_container"
>
<links-layer
:pipeline-data="pipelineStages"
@@ -148,10 +147,7 @@ export default {
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
- <div
- class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5"
- data-qa-selector="stage_container"
- >
+ <div class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5">
<stage-name :stage-name="stage.name" />
</div>
<div :class="$options.jobWrapperClasses">
@@ -162,7 +158,6 @@ export default {
:pipeline-id="$options.PIPELINE_ID"
:is-hovered="highlightedJob === group.name"
:is-faded-out="isFadedOut(group.name)"
- data-qa-selector="job_container"
@on-mouse-enter="setHoveredJob"
@on-mouse-leave="removeHoveredJob"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue b/app/assets/javascripts/ci/pipeline_editor/components/graph/stage_name.vue
index 600832b7633..600832b7633 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/graph/stage_name.vue
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
index ec6ee52b6b2..665ca907ed9 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -1,30 +1,11 @@
<script>
+import { GlCard } from '@gitlab/ui';
import PipelineStatus from './pipeline_status.vue';
import ValidationSegment from './validation_segment.vue';
-const baseClasses = ['gl-p-5', 'gl-bg-gray-10', 'gl-border-solid', 'gl-border-gray-100'];
-
-const pipelineStatusClasses = [
- ...baseClasses,
- 'gl-border-1',
- 'gl-border-b-0!',
- 'gl-rounded-top-base',
-];
-
-const validationSegmentClasses = [...baseClasses, 'gl-border-1', 'gl-rounded-base'];
-
-const validationSegmentWithPipelineStatusClasses = [
- ...baseClasses,
- 'gl-border-1',
- 'gl-rounded-bottom-left-base',
- 'gl-rounded-bottom-right-base',
-];
-
export default {
- pipelineStatusClasses,
- validationSegmentClasses,
- validationSegmentWithPipelineStatusClasses,
components: {
+ GlCard,
PipelineStatus,
ValidationSegment,
},
@@ -47,24 +28,19 @@ export default {
showPipelineStatus() {
return !this.isNewCiConfigFile;
},
- // make sure corners are rounded correctly depending on if
- // pipeline status is rendered
- validationStyling() {
- return this.showPipelineStatus
- ? this.$options.validationSegmentWithPipelineStatusClasses
- : this.$options.validationSegmentClasses;
- },
},
};
</script>
<template>
- <div class="gl-mb-5">
- <pipeline-status
- v-if="showPipelineStatus"
- :commit-sha="commitSha"
- :class="$options.pipelineStatusClasses"
- v-on="$listeners"
- />
- <validation-segment :class="validationStyling" :ci-config="ciConfigData" />
- </div>
+ <gl-card
+ class="gl-new-card gl-mb-3 gl-mt-0"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-py-4 gl-px-5"
+ >
+ <template v-if="showPipelineStatus" #header>
+ <pipeline-status :commit-sha="commitSha" v-on="$listeners" />
+ </template>
+
+ <validation-segment :ci-config="ciConfigData" />
+ </gl-card>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index f1c9770714a..f00098105d3 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -1,8 +1,8 @@
<script>
import { __ } from '~/locale';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
import { PIPELINE_FAILURE } from '../../constants';
export default {
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 3bce50224d9..58b5c0004e0 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
@@ -5,13 +5,10 @@ import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.query.graphql';
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
const POLL_INTERVAL = 10000;
@@ -141,7 +138,9 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap">
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-wrap gl-w-full"
+ >
<template v-if="showLoadingState">
<div>
<gl-loading-icon class="gl-mr-auto gl-display-inline-block" size="sm" />
@@ -149,20 +148,20 @@ export default {
</div>
</template>
<template v-else-if="hasError">
- <gl-icon class="gl-mr-auto" name="warning-solid" />
- <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
+ <div>
+ <gl-icon class="gl-mr-auto" name="warning-solid" />
+ <span data-testid="pipeline-error-msg">{{ $options.i18n.fetchError }}</span>
+ </div>
</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-icon :status="status" :size="16" data-testid="pipeline-status-icon" />
+ <ci-icon :status="status" :size="16" data-testid="pipeline-status-icon" class="gl-mr-2" />
</a>
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
- <span data-testid="pipeline-id" data-qa-selector="pipeline_id_content">
- {{ content }}{{ pipelineId }}
- </span>
+ <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
@@ -187,9 +186,8 @@ export default {
/>
<pipeline-editor-mini-graph v-else :pipeline="pipeline" v-on="$listeners" />
<gl-button
- class="gl-ml-3"
- category="secondary"
- variant="confirm"
+ class="gl-ml-3 gl-align-self-center"
+ size="small"
:href="status.detailsPath"
data-testid="pipeline-view-btn"
>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
index 8553256f13a..d54ad78b3d3 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
@@ -112,8 +112,8 @@ export default {
{{ $options.i18n.loading }}
</template>
<span v-else data-testid="validation-segment">
- <span class="gl-max-w-full" data-qa-selector="validation_message_content">
- <gl-icon :name="icon" />
+ <span class="gl-max-w-full">
+ <gl-icon :name="icon" class="gl-mr-2" />
<gl-sprintf :message="message">
<template v-if="hasLink" #link="{ content }">
<gl-link :href="helpPath">{{ content }}</gl-link>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
index a604d79259d..32eda355e66 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/job_assistant_drawer/utils.js
@@ -10,7 +10,8 @@ const trimText = (val) => (isString(val) ? trim(val) : val);
export const removeEmptyObj = (obj) => {
if (isArray(obj)) {
return reject(map(obj, removeEmptyObj), isEmptyValue);
- } else if (isObject(obj)) {
+ }
+ if (isObject(obj)) {
return omitBy(mapValues(obj, removeEmptyObj), isEmptyValue);
}
return obj;
@@ -19,7 +20,8 @@ export const removeEmptyObj = (obj) => {
export const trimFields = (data) => {
if (isArray(data)) {
return data.map(trimFields);
- } else if (isObject(data)) {
+ }
+ if (isObject(data)) {
return mapValues(data, trimFields);
}
return trimText(data);
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index a954615ca8a..c7c15cdd76e 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -2,7 +2,7 @@
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import { s__, __ } from '~/locale';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import {
CREATE_TAB,
@@ -182,7 +182,7 @@ export default {
<template>
<gl-tabs
class="file-editor gl-mb-3"
- data-qa-selector="file_editor_container"
+ data-testid="file-editor-container"
:query-param-name="$options.query.TAB_QUERY_PARAM"
sync-active-tab-with-query-params
>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
index efa6a54c638..57694bbcd77 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
@@ -42,7 +42,6 @@ export default {
target="file-tree-toggle"
triggers="manual"
placement="right"
- data-qa-selector="file_tree_popover"
@close-button-clicked="dismissPermanently"
>
<div v-outside="dismissPermanently" class="gl-font-base gl-mb-3">
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 25e4e99bf54..90402a89280 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -61,8 +61,7 @@ export default {
<gl-button
variant="confirm"
class="gl-mt-3"
- data-testid="create_new_ci_button"
- data-qa-selector="create_new_ci_button"
+ data-testid="create-new-ci-button"
@click="createEmptyConfigFile"
>
{{ $options.i18n.btnText }}
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 7583fa7a3b5..617088f303b 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
@@ -246,7 +246,6 @@ export default {
class="gl-mt-3"
:disabled="isInitialCiContentLoading"
data-testid="simulate-pipeline-button"
- data-qa-selector="simulate_pipeline_button"
@click="validateYaml"
>
{{ $options.i18n.cta }}
diff --git a/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
index 5354ed7c2d5..3570fc1f008 100644
--- a/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
@@ -1,4 +1,4 @@
-#import "~/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql"
+#import "~/ci/pipeline_details/graphql/fragments/pipeline_stages_connection.fragment.graphql"
query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) {
ciConfig(projectPath: $projectPath, sha: $sha, content: $content) {
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
index de8e5a1a284..49562b0be28 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
@@ -4,7 +4,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { mergeUrlParams, queryToObject, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { __, s__ } from '~/locale';
-import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
+import { unwrapStagesWithNeeds } from '~/ci/pipeline_details/utils/unwrapping_utils';
import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes_dialog.vue';
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
@@ -394,7 +394,7 @@ export default {
</script>
<template>
- <div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
+ <div class="gl-mt-4 gl-relative">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
v-else-if="showStartScreen || usesExternalConfig"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js b/app/assets/javascripts/ci/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
index 1ca9e35c008..1ca9e35c008 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stage.query.graphql b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql
index 64a5964dbeb..64a5964dbeb 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stage.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stage.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql
index 69a29947b16..69a29947b16 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_stages.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/job_item.vue
index 7f97097def6..7f97097def6 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/job_item.vue
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index d6e585d093b..d20d4aec59d 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -1,11 +1,11 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
+import ActionComponent from '~/ci/common/private/job_action_component.vue';
+import JobNameComponent from '~/ci/common/private/job_name_component.vue';
+import { ICONS } from '~/ci/constants';
+import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { s__, sprintf } from '~/locale';
-import { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { ICONS } from '../../constants';
+import { reportToSentry } from '~/ci/utils';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
index 8c0e65d1d39..8c0e65d1d39 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index 048e42731c7..bbe0f1fbefc 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -15,9 +15,9 @@
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
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';
import { __, s__, sprintf } from '~/locale';
-import eventHub from '../../event_hub';
import LegacyJobItem from './legacy_job_item.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index a5c6dc98694..8567654a89e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -105,7 +105,7 @@ export default {
v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
:href="pipeline.path"
:class="triggerButtonClass(pipeline)"
- class="linked-pipeline-mini-item gl-display-inline-block gl-h-6 gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
+ class="linked-pipeline-mini-item gl-display-inline-flex gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
data-testid="linked-pipeline-mini-item"
>
<ci-icon
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_mini_graph.vue
index 7cdaec81466..358d3dc826e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_mini_graph.vue
@@ -2,14 +2,11 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from './graphql/queries/get_pipeline_stages.query.graphql';
import LegacyPipelineMiniGraph from './legacy_pipeline_mini_graph.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stage.vue
index 8e22f440089..747b5d33b1a 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stage.vue
@@ -1,12 +1,9 @@
<script>
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import getPipelineStageQuery from '~/pipelines/graphql/queries/get_pipeline_stage.query.graphql';
+import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/ci/pipeline_details/constants';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import getPipelineStageQuery from './graphql/queries/get_pipeline_stage.query.graphql';
import JobItem from './job_item.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stages.vue
index 02dba9ba30f..f883833f7ea 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/pipeline_stages.vue
@@ -42,7 +42,7 @@ export default {
<div
v-for="stage in stages"
:key="stage.name"
- class="pipeline-mini-graph-stage-container dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle"
+ class="pipeline-mini-graph-stage-container dropdown gl-display-inline-flex gl-mr-2 gl-my-2 gl-vertical-align-middle"
>
<pipeline-stage
v-if="isGraphql"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
index fbdb60f61f1..f701bedc74d 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
@@ -41,6 +41,7 @@ export default {
<template>
<gl-empty-state
:svg-path="$options.SCHEDULE_MD_SVG_URL"
+ :svg-height="150"
:primary-button-text="$options.i18n.createNew"
:primary-button-link="newSchedulePath"
>
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 396ff9808f2..0c3ede47015 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
@@ -1,8 +1,7 @@
<script>
import {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlFormCheckbox,
GlForm,
GlFormGroup,
@@ -27,8 +26,7 @@ const scheduleId = queryToObject(window.location.search).id;
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlForm,
GlFormCheckbox,
GlFormGroup,
@@ -81,7 +79,7 @@ export default {
this.description = schedule.description;
this.cron = schedule.cron;
this.cronTimezone = schedule.cronTimezone;
- this.scheduleRef = schedule.ref;
+ this.scheduleRef = schedule.ref || this.defaultBranch;
this.variables = variables.map((variable) => {
return {
id: variable.id,
@@ -144,10 +142,6 @@ export default {
revealText: __('Reveal values'),
hideText: __('Hide values'),
},
- typeOptions: {
- [VARIABLE_TYPE]: __('Variable'),
- [FILE_TYPE]: __('File'),
- },
formElementClasses: 'gl-md-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
computed: {
dropdownTranslations() {
@@ -155,7 +149,7 @@ export default {
dropdownHeader: this.$options.i18n.targetBranchTag,
};
},
- typeOptionsListbox() {
+ typeOptions() {
return [
{
text: __('Variable'),
@@ -232,9 +226,9 @@ export default {
empty: true,
});
},
- setVariableAttribute(key, attribute, value) {
+ setVariableType(typeValue, key) {
const variable = this.variables.find((v) => v.key === key);
- variable[attribute] = value;
+ variable.variableType = typeValue;
},
removeVariable(index) {
this.variables[index].destroy = true;
@@ -387,19 +381,15 @@ export default {
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
>
- <gl-dropdown
- :text="$options.typeOptions[variable.variableType]"
+ <gl-collapsible-listbox
+ :items="typeOptions"
+ :selected="variable.variableType"
:class="$options.formElementClasses"
+ block
data-testid="pipeline-form-ci-variable-type"
- >
- <gl-dropdown-item
- v-for="type in Object.keys($options.typeOptions)"
- :key="type"
- @click="setVariableAttribute(variable.key, 'variableType', type)"
- >
- {{ $options.typeOptions[type] }}
- </gl-dropdown-item>
- </gl-dropdown>
+ @select="setVariableType($event, variable.key)"
+ />
+
<gl-form-input
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
@@ -414,7 +404,6 @@ export default {
value="*****************"
disabled
class="gl-mb-3 gl-h-7!"
- :style="$options.textAreaStyle"
:no-resize="false"
data-testid="pipeline-form-ci-variable-hidden-value"
/>
@@ -424,7 +413,6 @@ export default {
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3 gl-h-7!"
- :style="$options.textAreaStyle"
:no-resize="false"
data-testid="pipeline-form-ci-variable-value"
data-qa-selector="ci_variable_value_field"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
index 08efa794bcc..56d50026f17 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
@@ -27,10 +27,13 @@ export default {
</script>
<template>
- <div>
- <gl-icon :name="iconName" />
+ <div data-testid="pipeline-schedule-target">
<span v-if="refPath">
+ <gl-icon :name="iconName" />
<gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link>
</span>
+ <span v-else>
+ {{ s__('PipelineSchedules|None') }}
+ </span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue b/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue
deleted file mode 100644
index b4d84309c5f..00000000000
--- a/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<script>
-import { GlModal } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-
-export default {
- components: {
- GlModal,
- },
- props: {
- ownershipUrl: {
- type: String,
- required: true,
- },
- },
- modalId: 'pipeline-take-ownership-modal',
- i18n: {
- takeOwnership: s__('PipelineSchedules|Take ownership'),
- ownershipMessage: s__(
- 'PipelineSchedules|Only the owner of a pipeline schedule can make changes to it. Do you want to take ownership of this schedule?',
- ),
- cancelLabel: __('Cancel'),
- },
- computed: {
- actionCancel() {
- return { text: this.$options.i18n.cancelLabel };
- },
- actionPrimary() {
- return {
- text: this.$options.i18n.takeOwnership,
- attributes: {
- variant: 'confirm',
- category: 'primary',
- href: this.ownershipUrl,
- 'data-method': 'post',
- },
- };
- },
- },
-};
-</script>
-<template>
- <gl-modal
- :modal-id="$options.modalId"
- :action-primary="actionPrimary"
- :action-cancel="actionCancel"
- :title="$options.i18n.takeOwnership"
- >
- <p>{{ $options.i18n.ownershipMessage }}</p>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ci_templates.vue
index 439dc0eb253..439dc0eb253 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ci_templates.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
index 5208f9a3ce7..1a2021df9c8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/ios_templates.vue
@@ -4,7 +4,7 @@ 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 '~/pipelines/graphql/provider';
+import apolloProvider from '~/ci/pipeline_details/graphql/provider';
import CiTemplates from './ci_templates.vue';
export default {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
index 3bbdfc73e1b..6e7d6908cd9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/no_ci_empty_state.vue
@@ -2,8 +2,8 @@
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
-import PipelinesCiTemplates from './empty_state/pipelines_ci_templates.vue';
-import IosTemplates from './empty_state/ios_templates.vue';
+import PipelinesCiTemplates from './pipelines_ci_templates.vue';
+import IosTemplates from './ios_templates.vue';
export default {
i18n: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
index a6297213402..a6297213402 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
index edf4cc87a87..82f1d57912a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
@@ -5,8 +5,8 @@ import { __, s__, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
-import { BRIDGE_KIND } from '~/pipelines/components/graph/constants';
-import RetryMrFailedJobMutation from '../../../graphql/mutations/retry_mr_failed_job.mutation.graphql';
+import { BRIDGE_KIND } from '~/ci/pipeline_details/graph/constants';
+import RetryMrFailedJobMutation from '~/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql';
export default {
components: {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue
index 2c5aa84bc4f..138269bdb8a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue
@@ -2,9 +2,10 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__, sprintf } from '~/locale';
-import { getQueryHeaders } from '~/pipelines/components/graph/utils';
-import getPipelineFailedJobs from '../../../graphql/queries/get_pipeline_failed_jobs.query.graphql';
-import { graphqlEtagPipelinePath, sortJobsByStatus } from './utils';
+import { getQueryHeaders } from '~/ci/pipeline_details/graph/utils';
+import { graphqlEtagPipelinePath } from '~/ci/pipeline_details/utils';
+import getPipelineFailedJobs from '~/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql';
+import { sortJobsByStatus } from './utils';
import FailedJobDetails from './failed_job_details.vue';
const POLL_INTERVAL = 10000;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue
index 60c429459bf..c01037e9791 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue
@@ -91,7 +91,7 @@ export default {
<template #header>
<gl-button
variant="link"
- class="gl-text-gray-700! gl-font-weight-semibold"
+ class="gl-text-gray-500! gl-font-weight-semibold"
@click="toggleWidget"
>
<gl-icon :name="iconName" />
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/utils.js
index 2d0c467c54f..3f395fff7e0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/utils.js
@@ -13,7 +13,3 @@ export const sortJobsByStatus = (jobs = []) => {
return 1;
});
};
-
-export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
- return `${graphqlPath}pipelines/id/${pipelineId}`;
-};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
index 235126fea0c..235126fea0c 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_labels.vue
index 40b2454b8c1..082ede60244 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/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 '../../constants';
+import { SCHEDULE_ORIGIN } from '~/ci/pipeline_details/constants';
export default {
components: {
@@ -54,7 +54,7 @@ export default {
v-gl-tooltip
:href="pipelineScheduleUrl"
target="__blank"
- :title="__('This pipeline was triggered by a schedule.')"
+ :title="__('This pipeline was created by a schedule.')"
variant="info"
size="sm"
data-testid="pipeline-url-scheduled"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_multi_actions.vue
index 747d94d92f2..78acead95f4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_multi_actions.vue
@@ -1,8 +1,7 @@
<script>
import {
GlAlert,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
GlSearchBoxByType,
GlLoadingIcon,
GlTooltipDirective,
@@ -14,6 +13,7 @@ import Tracking from '~/tracking';
import { TRACKING_CATEGORIES } from '../../constants';
export const i18n = {
+ searchPlaceholder: __('Search artifacts'),
downloadArtifacts: __('Download artifacts'),
artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
artifactsFetchWarningMessage: s__(
@@ -29,8 +29,7 @@ export default {
},
components: {
GlAlert,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
GlSearchBoxByType,
GlLoadingIcon,
},
@@ -67,6 +66,17 @@ export default {
? fuzzaldrinPlus.filter(this.artifacts, this.searchQuery, { key: 'name' })
: this.artifacts;
},
+ items() {
+ return this.filteredArtifacts.map(({ name, path }) => ({
+ text: name,
+ href: path,
+ extraAttrs: {
+ download: '',
+ rel: 'nofollow',
+ 'data-testid': 'artifact-item',
+ },
+ }));
+ },
},
watch: {
pipelineId() {
@@ -107,66 +117,73 @@ export default {
this.isLoading = false;
});
},
- handleDropdownShown() {
- if (this.hasArtifacts) {
- this.$refs.searchInput.focusInput();
- }
+ onDisclosureDropdownShown() {
+ this.fetchArtifacts();
+ },
+ onDisclosureDropdownHidden() {
+ this.searchQuery = '';
},
},
};
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-gl-tooltip
+ class="gl-text-left"
:title="$options.i18n.downloadArtifacts"
- :text="$options.i18n.downloadArtifacts"
+ :toggle-text="$options.i18n.downloadArtifacts"
:aria-label="$options.i18n.downloadArtifacts"
- :header-text="$options.i18n.downloadArtifacts"
+ :items="items"
icon="download"
- data-testid="pipeline-multi-actions-dropdown"
- right
- lazy
+ placement="right"
text-sr-only
- @show="fetchArtifacts"
- @shown="handleDropdownShown"
+ data-testid="pipeline-multi-actions-dropdown"
+ @shown="onDisclosureDropdownShown"
+ @hidden="onDisclosureDropdownHidden"
>
- <gl-alert v-if="hasError && !hasArtifacts" variant="danger" :dismissible="false">
- {{ $options.i18n.artifactsFetchErrorMessage }}
- </gl-alert>
-
- <gl-loading-icon v-else-if="isLoading" size="sm" />
-
- <gl-dropdown-item v-else-if="!hasArtifacts" data-testid="artifacts-empty-message">
- {{ $options.i18n.emptyArtifactsMessage }}
- </gl-dropdown-item>
-
<template #header>
- <gl-search-box-by-type v-if="hasArtifacts" ref="searchInput" v-model.trim="searchQuery" />
+ <div
+ aria-hidden="true"
+ class="gl-display-flex gl-align-items-center gl-p-4! gl-min-h-8 gl-font-sm gl-font-weight-bold gl-text-gray-900 gl-border-b-1 gl-border-b-solid gl-border-b-gray-200"
+ >
+ {{ $options.i18n.downloadArtifacts }}
+ </div>
+ <div v-if="hasArtifacts" class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-200">
+ <gl-search-box-by-type
+ ref="searchInput"
+ v-model.trim="searchQuery"
+ :placeholder="$options.i18n.searchPlaceholder"
+ borderless
+ autofocus
+ />
+ </div>
+ <gl-alert
+ v-if="hasError && !hasArtifacts"
+ variant="danger"
+ :dismissible="false"
+ data-testid="artifacts-fetch-error"
+ >
+ {{ $options.i18n.artifactsFetchErrorMessage }}
+ </gl-alert>
</template>
- <gl-dropdown-item
- v-for="(artifact, i) in filteredArtifacts"
- :key="i"
- :href="artifact.path"
- rel="nofollow"
- download
- class="gl-word-break-word"
- data-testid="artifact-item"
+ <gl-loading-icon v-if="isLoading" class="gl-m-3" size="sm" />
+ <p
+ v-else-if="filteredArtifacts.length === 0"
+ class="gl-px-4 gl-py-3 gl-m-0 gl-text-gray-600"
+ data-testid="artifacts-empty-message"
>
- {{ artifact.name }}
- </gl-dropdown-item>
+ {{ $options.i18n.emptyArtifactsMessage }}
+ </p>
<template #footer>
- <gl-dropdown-item
+ <p
v-if="hasError && hasArtifacts"
- class="gl-list-style-none"
- disabled
+ class="gl-font-sm gl-text-secondary gl-py-4 gl-px-5 gl-mb-0 gl-border-t"
data-testid="artifacts-fetch-warning"
>
- <span class="gl-font-sm">
- {{ $options.i18n.artifactsFetchWarningMessage }}
- </span>
- </gl-dropdown-item>
+ {{ $options.i18n.artifactsFetchWarningMessage }}
+ </p>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
index caeee7edefe..b05bdae65c4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
+import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '~/ci/constants';
import eventHub from '../../event_hub';
-import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '../../constants';
import PipelineMultiActions from './pipeline_multi_actions.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index 9f38be668f2..9f38be668f2 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
index 2a73795db0a..2a73795db0a 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_triggerer.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
index ff1a01d5037..edaeb481d7b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_url.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
import Tracking from '~/tracking';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { ICONS, TRACKING_CATEGORIES } from '../../constants';
+import { ICONS, TRACKING_CATEGORIES } from '~/ci/constants';
import PipelineLabels from './pipeline_labels.vue';
export default {
@@ -151,7 +151,10 @@ export default {
<div v-if="!pipelineName" class="commit-title gl-mb-2" data-testid="commit-title-container">
<span v-if="commitTitle" class="gl-display-flex">
- <tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate">
+ <tooltip-on-truncate
+ :title="commitTitle"
+ class="gl-flex-grow-1 gl-text-truncate gl-p-3 gl-ml-n3 gl-mr-n3 gl-mt-n3 gl-mb-n3"
+ >
<gl-link
:href="commitUrl"
class="commit-row-message gl-text-blue-600!"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
index 4452db64b0a..3021b4a2ef8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
@@ -60,7 +60,7 @@ export default {
<gl-disclosure-dropdown
v-if="shouldShowDropdown"
v-gl-tooltip
- class="build-artifacts js-pipeline-dropdown-download"
+ class="gl-text-left"
:title="$options.i18n.artifacts"
:toggle-text="$options.i18n.artifacts"
:aria-label="$options.i18n.artifacts"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_filtered_search.vue
index 7dc1e60610e..6aadb6b73c8 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_filtered_search.vue
@@ -5,11 +5,11 @@ import { s__ } from '~/locale';
import Tracking from '~/tracking';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { TRACKING_CATEGORIES } from '../../constants';
-import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
-import PipelineSourceToken from './tokens/pipeline_source_token.vue';
-import PipelineStatusToken from './tokens/pipeline_status_token.vue';
-import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
-import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
+import PipelineBranchNameToken from '../tokens/pipeline_branch_name_token.vue';
+import PipelineSourceToken from '../tokens/pipeline_source_token.vue';
+import PipelineStatusToken from '../tokens/pipeline_status_token.vue';
+import PipelineTagNameToken from '../tokens/pipeline_tag_name_token.vue';
+import PipelineTriggerAuthorToken from '../tokens/pipeline_trigger_author_token.vue';
export default {
userType: 'username',
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
index 262e82677a7..4dacd474bde 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
@@ -8,7 +8,7 @@ import Tracking from '~/tracking';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import eventHub from '../../event_hub';
import { TRACKING_CATEGORIES } from '../../constants';
-import getPipelineActionsQuery from '../../graphql/queries/get_pipeline_actions.query.graphql';
+import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
export default {
name: 'PipelinesManualActions',
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
index 00ab8a25ca1..2da9141df8e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_status_badge.vue
@@ -1,5 +1,6 @@
<script>
-import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
+import { CHILD_VIEW } from '~/ci/pipeline_details/constants';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/ci/pipelines_page/components/time_ago.vue
index 70343544638..70343544638 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/time_ago.vue
diff --git a/app/assets/javascripts/ci/pipelines_page/constants.js b/app/assets/javascripts/ci/pipelines_page/constants.js
new file mode 100644
index 00000000000..aa6ef8a25ee
--- /dev/null
+++ b/app/assets/javascripts/ci/pipelines_page/constants.js
@@ -0,0 +1,2 @@
+export const ANY_TRIGGER_AUTHOR = 'Any';
+export const FILTER_PIPELINES_SEARCH_DELAY = 200;
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_actions.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql
index d1878c01e91..d1878c01e91 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_actions.query.graphql
+++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_actions.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql
index 6b553866f63..6b553866f63 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql
+++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql
diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs_count.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql
index b70e95deab6..b70e95deab6 100644
--- a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_failed_jobs_count.query.graphql
+++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs_count.query.graphql
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index 574d291a767..87ee5463bb0 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -7,29 +7,29 @@ import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/alert';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
-import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { isLoggedIn } from '~/lib/utils/common_utils';
-import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import {
- ANY_TRIGGER_AUTHOR,
- RAW_TEXT_WARNING,
FILTER_TAG_IDENTIFIER,
PipelineKeyOptions,
+ RAW_TEXT_WARNING,
TRACKING_CATEGORIES,
-} from '../../constants';
-import PipelinesMixin from '../../mixins/pipelines_mixin';
-import PipelinesService from '../../services/pipelines_service';
-import { validateParams } from '../../utils';
-import EmptyState from './empty_state.vue';
-import NavigationControls from './nav_controls.vue';
-import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
-import PipelinesTableComponent from './pipelines_table.vue';
+} from '~/ci/constants';
+import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
+import { validateParams } from '~/ci/pipeline_details/utils';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
+import { isLoggedIn } from '~/lib/utils/common_utils';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import PipelinesService from './services/pipelines_service';
+import { ANY_TRIGGER_AUTHOR } from './constants';
+import NoCiEmptyState from './components/empty_state/no_ci_empty_state.vue';
+import NavigationControls from './components/nav_controls.vue';
+import PipelinesFilteredSearch from './components/pipelines_filtered_search.vue';
export default {
PipelineKeyOptions,
components: {
- EmptyState,
+ NoCiEmptyState,
GlCollapsibleListbox,
GlEmptyState,
GlIcon,
@@ -409,7 +409,7 @@ export default {
class="prepend-top-20"
/>
- <empty-state
+ <no-ci-empty-state
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
@@ -426,6 +426,7 @@ export default {
<gl-empty-state
v-else-if="stateToRender === $options.stateMap.emptyTab"
:svg-path="noPipelinesSvgPath"
+ :svg-height="150"
:title="emptyTabMessage"
/>
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/ci/pipelines_page/services/pipelines_service.js
index 3ec563c95bb..c38fa07c7e3 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/ci/pipelines_page/services/pipelines_service.js
@@ -1,6 +1,6 @@
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import { validateParams } from '../utils';
+import { validateParams } from '../../pipeline_details/utils';
export default class PipelinesService {
/**
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js b/app/assets/javascripts/ci/pipelines_page/tokens/constants.js
index d8f15cfde91..d8f15cfde91 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/constants.js
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_branch_name_token.vue
index 81f46d5f2f9..45b6fb380a9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_branch_name_token.vue
@@ -3,7 +3,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from
import { debounce } from 'lodash';
import Api from '~/api';
import { createAlert } from '~/alert';
-import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
+import { __ } from '~/locale';
+import { FILTER_PIPELINES_SEARCH_DELAY } from '../constants';
export default {
components: {
@@ -46,7 +47,7 @@ export default {
})
.catch((err) => {
createAlert({
- message: FETCH_BRANCH_ERROR_MESSAGE,
+ message: __('There was a problem fetching project branches.'),
});
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_source_token.vue
index 9643ddfbd21..b4b5c5c1b37 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_source_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants';
+import { PIPELINE_SOURCES } from 'ee_else_ce/ci/pipelines_page/tokens/constants';
export default {
PIPELINE_SOURCES,
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_status_token.vue
index 020a08b8cee..020a08b8cee 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_status_token.vue
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_tag_name_token.vue
index b32f5de2d7e..a6034e78b6d 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_tag_name_token.vue
@@ -3,7 +3,8 @@ import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from
import { debounce } from 'lodash';
import Api from '~/api';
import { createAlert } from '~/alert';
-import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
+import { __ } from '~/locale';
+import { FILTER_PIPELINES_SEARCH_DELAY } from '../constants';
export default {
components: {
@@ -39,7 +40,7 @@ export default {
})
.catch((err) => {
createAlert({
- message: FETCH_TAG_ERROR_MESSAGE,
+ message: __('There was a problem fetching project tags.'),
});
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue
index a89354c671a..20c5e1557a7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ b/app/assets/javascripts/ci/pipelines_page/tokens/pipeline_trigger_author_token.vue
@@ -9,11 +9,8 @@ import {
import { debounce } from 'lodash';
import Api from '~/api';
import { createAlert } from '~/alert';
-import {
- ANY_TRIGGER_AUTHOR,
- FETCH_AUTHOR_ERROR_MESSAGE,
- FILTER_PIPELINES_SEARCH_DELAY,
-} from '../../../constants';
+import { __ } from '~/locale';
+import { ANY_TRIGGER_AUTHOR, FILTER_PIPELINES_SEARCH_DELAY } from '../constants';
export default {
anyTriggerAuthor: ANY_TRIGGER_AUTHOR,
@@ -62,7 +59,7 @@ export default {
})
.catch((err) => {
createAlert({
- message: FETCH_AUTHOR_ERROR_MESSAGE,
+ message: __('There was a problem fetching project users.'),
});
this.loading = false;
throw err;
diff --git a/app/assets/javascripts/ci/reports/components/issue_status_icon.vue b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
index bd41b8d23f1..f2346a5512e 100644
--- a/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
@@ -22,7 +22,8 @@ export default {
iconName() {
if (this.isStatusFailed) {
return 'status_failed_borderless';
- } else if (this.isStatusSuccess) {
+ }
+ if (this.isStatusSuccess) {
return 'status_success_borderless';
}
@@ -49,6 +50,6 @@ export default {
}"
class="report-block-list-icon"
>
- <gl-icon :name="iconName" :size="statusIconSize" :data-qa-selector="`status_${status}_icon`" />
+ <gl-icon :name="iconName" :size="statusIconSize" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index a4ec7b6a325..fd6c6cca6b7 100644
--- a/app/assets/javascripts/ci/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
@@ -159,7 +159,8 @@ export default {
slotName() {
if (this.isSuccess) {
return SLOT_SUCCESS;
- } else if (this.isLoading) {
+ }
+ if (this.isLoading) {
return SLOT_LOADING;
}
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 e6813211fe9..0ec94dc865f 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
@@ -178,6 +178,22 @@ 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-dashboard-link />
+ <gl-button :href="newRunnerPath" variant="confirm">
+ {{ s__('Runners|New instance runner') }}
+ </gl-button>
+ <registration-dropdown
+ :registration-token="registrationToken"
+ :type="$options.INSTANCE_TYPE"
+ placement="right"
+ />
+ </div>
+ </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"
>
@@ -189,18 +205,6 @@ export default {
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
-
- <div class="gl-w-full gl-md-w-auto gl-display-flex gl-gap-3">
- <runner-dashboard-link />
- <gl-button :href="newRunnerPath" variant="confirm">
- {{ s__('Runners|New instance runner') }}
- </gl-button>
- <registration-dropdown
- :registration-token="registrationToken"
- :type="$options.INSTANCE_TYPE"
- placement="right"
- />
- </div>
</div>
<runner-filtered-search-bar
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
index cb43760b2d6..8f1c7234b84 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
@@ -50,12 +50,7 @@ export default {
<template>
<div>
- <gl-link
- v-if="cell.href"
- v-gl-tooltip="cell.tooltip"
- :href="cell.href"
- class="gl-text-body gl-text-decoration-underline"
- >
+ <gl-link v-if="cell.href" v-gl-tooltip="cell.tooltip" :href="cell.href" class="gl-text-body">
{{ cell.text }}
</gl-link>
<span v-else>{{ cell.text }}</span>
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 cc31afea88c..a80d6207be8 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
@@ -12,7 +12,6 @@ import RunnerManagersBadge from '../runner_managers_badge.vue';
import { formatJobCount } from '../../utils';
import {
- I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -73,7 +72,6 @@ export default {
formatNumber,
},
i18n: {
- I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -100,7 +98,10 @@ export default {
<runner-type-badge :type="runner.runnerType" size="sm" class="gl-vertical-align-middle" />
</div>
- <div class="gl-mb-3 gl-ml-auto gl-display-inline-flex gl-max-w-full">
+ <div
+ v-if="runner.version || runner.description"
+ class="gl-mb-3 gl-ml-auto gl-display-inline-flex gl-max-w-full gl-font-sm gl-align-items-center"
+ >
<template v-if="runner.version">
<div class="gl-flex-shrink-0">
<runner-upgrade-status-icon :upgrade-status="runner.upgradeStatus" />
@@ -108,19 +109,20 @@ export default {
<template #version>{{ runner.version }}</template>
</gl-sprintf>
</div>
- <div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div>
+ <div v-if="runner.description" class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div>
</template>
<tooltip-on-truncate
+ v-if="runner.description"
class="gl-text-truncate gl-display-block"
:class="{ 'gl-text-secondary': !runner.description }"
:title="runner.description"
>
- {{ runner.description || $options.i18n.I18N_NO_DESCRIPTION }}
+ {{ runner.description }}
</tooltip-on-truncate>
</div>
- <div>
- <runner-summary-field icon="clock">
+ <div class="gl-font-sm">
+ <runner-summary-field icon="clock" icon-size="sm">
<gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL">
<template #timeAgo>
<time-ago v-if="runner.contactedAt" :time="runner.contactedAt" />
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
index 742259ee491..b1b61e03eec 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
@@ -25,7 +25,7 @@ export default {
<template>
<div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-mb-3 gl-mr-4">
- <gl-icon v-if="icon" :name="icon" />
+ <gl-icon v-if="icon" :name="icon" :size="12" />
<!-- display tooltip as a label for screen readers -->
<span class="gl-sr-only">{{ tooltip }}</span>
<slot></slot>
diff --git a/app/assets/javascripts/ci/runner/components/runner_create_form.vue b/app/assets/javascripts/ci/runner/components/runner_create_form.vue
index 1b363174d28..adaed77055a 100644
--- a/app/assets/javascripts/ci/runner/components/runner_create_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_create_form.vue
@@ -120,7 +120,7 @@ export default {
</script>
<template>
<gl-form @submit.prevent="onSubmit">
- <runner-form-fields v-model="runner" />
+ <runner-form-fields v-model="runner" :runner-type="runnerType" />
<div class="gl-display-flex gl-mt-6">
<gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="saving">
diff --git a/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
index 3634dcf1c93..81b2a17631e 100644
--- a/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
@@ -92,7 +92,6 @@ export default {
:initial-filter-value="initialFilterValue"
:tokens="validTokens"
:initial-sort-by="initialSortBy"
- :search-input-placeholder="__('Search or filter results...')"
:search-text-option-label="s__('Runners|Search description...')"
terms-as-tokens
data-testid="runners-filtered-search"
diff --git a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
index d090a562ff7..38e36733045 100644
--- a/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_form_fields.vue
@@ -10,7 +10,12 @@ import {
GlSkeletonLoader,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
+import {
+ ACCESS_LEVEL_NOT_PROTECTED,
+ ACCESS_LEVEL_REF_PROTECTED,
+ PROJECT_TYPE,
+ RUNNER_TYPES,
+} from '../constants';
export default {
name: 'RunnerFormFields',
@@ -26,6 +31,12 @@ export default {
import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'),
},
props: {
+ runnerType: {
+ type: String,
+ required: false,
+ default: null,
+ validator: (t) => RUNNER_TYPES.includes(t),
+ },
value: {
type: Object,
default: null,
@@ -44,7 +55,7 @@ export default {
},
computed: {
canBeLockedToProject() {
- return this.value?.runnerType === PROJECT_TYPE;
+ return this.runnerType === PROJECT_TYPE;
},
},
watch: {
diff --git a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
index 10790c398b0..58244e1f2df 100644
--- a/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_managers_table.vue
@@ -6,6 +6,7 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
import { I18N_STATUS_NEVER_CONTACTED } from '../constants';
import RunnerStatusBadge from './runner_status_badge.vue';
+import RunnerJobStatusBadge from './runner_job_status_badge.vue';
export default {
name: 'RunnerManagersTable',
@@ -15,6 +16,7 @@ export default {
HelpPopover,
GlIntersperse,
RunnerStatusBadge,
+ RunnerJobStatusBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
},
@@ -52,7 +54,15 @@ export default {
</help-popover>
</template>
<template #cell(status)="{ item = {} }">
- <runner-status-badge :contacted-at="item.contactedAt" :status="item.status" />
+ <runner-status-badge
+ class="gl-vertical-align-middle"
+ :contacted-at="item.contactedAt"
+ :status="item.status"
+ />
+ <runner-job-status-badge
+ class="gl-vertical-align-middle"
+ :job-status="item.jobExecutionStatus"
+ />
</template>
<template #cell(version)="{ item = {} }">
{{ item.version }}
diff --git a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
index a49641194a7..a841f66b566 100644
--- a/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_platforms_radio_group.vue
@@ -55,7 +55,7 @@ export default {
<div class="gl-mt-3 gl-mb-6">
<label>{{ s__('Runners|Operating systems') }}</label>
- <div class="gl-display-flex gl-flex-wrap gl-gap-5">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<runner-platforms-radio
v-model="model"
@@ -76,7 +76,7 @@ export default {
<div class="gl-mt-3 gl-mb-6">
<label>{{ s__('Runners|Containers') }}</label>
- <div class="gl-display-flex gl-flex-wrap gl-gap-5">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-3">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
<runner-platforms-radio :image="$options.DOCKER_LOGO_URL">
<gl-link :href="$options.DOCKER_HELP_URL" target="_blank">
diff --git a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
index 70226074993..3af10c59e31 100644
--- a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
@@ -112,7 +112,12 @@ export default {
:scope="countScope"
:variables="tabBadgeCountVariables(tab.runnerType)"
>
- <gl-badge v-if="tabCount(count)" class="gl-ml-1" size="sm">
+ <gl-badge
+ v-if="tabCount(count)"
+ class="gl-ml-1"
+ size="sm"
+ :data-testid="`runner-count-${tab.title.toLowerCase()}`"
+ >
{{ tabCount(count) }}
</gl-badge>
</runner-count>
diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
index 6b94e594f1c..4278615ba66 100644
--- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
@@ -96,7 +96,7 @@ export default {
</script>
<template>
<gl-form @submit.prevent="onSubmit">
- <runner-form-fields v-model="model" :loading="loading" />
+ <runner-form-fields v-model="model" :loading="loading" :runner-type="runnerType" />
<runner-update-cost-factor-fields v-model="model" :runner-type="runnerType" />
<div class="gl-mt-6">
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
index c33c42f3afe..cee1088d90b 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
@@ -55,7 +55,8 @@ export default {
query() {
if (this.scope === INSTANCE_TYPE) {
return allRunnersCountQuery;
- } else if (this.scope === GROUP_TYPE) {
+ }
+ if (this.scope === GROUP_TYPE) {
return groupRunnersCountQuery;
}
return null;
@@ -74,7 +75,8 @@ export default {
update(data) {
if (this.scope === INSTANCE_TYPE) {
return data?.runners?.count;
- } else if (this.scope === GROUP_TYPE) {
+ }
+ if (this.scope === GROUP_TYPE) {
return data?.group?.runners?.count;
}
return null;
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
index e0a6f4b1e67..6c49263ac82 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
@@ -90,6 +90,7 @@ export default {
:scope="scope"
v-bind="stat.props"
class="gl-px-5"
+ :data-testid="`runner-stats-${stat.key.toLowerCase()}`"
/>
<runner-upgrade-status-stats
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index 203f97876de..3293c68ddb8 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -15,7 +15,7 @@ export const I18N_CREATE_ERROR = s__(
);
export const FILTER_CSS_CLASSES =
- 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1';
+ 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1';
// Type
@@ -96,7 +96,6 @@ export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
// List
-export const I18N_NO_DESCRIPTION = s__('Runners|No description');
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.',
);
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
index ead005d1252..84d32e24f24 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_manager_shared.fragment.graphql
@@ -9,4 +9,5 @@ fragment CiRunnerManagerShared on CiRunnerManager {
platformName
ipAddress
contactedAt
+ jobExecutionStatus
}
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 71584c40a38..dcaf8635f5c 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
@@ -212,6 +212,27 @@ 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">
+ <gl-button
+ v-if="newRunnerPath"
+ :href="newRunnerPath"
+ variant="confirm"
+ data-testid="new-group-runner-button"
+ >
+ {{ s__('Runners|New group runner') }}
+ </gl-button>
+ <registration-dropdown
+ v-if="registrationToken"
+ :registration-token="registrationToken"
+ :type="$options.GROUP_TYPE"
+ placement="right"
+ />
+ </div>
+ </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"
>
@@ -225,19 +246,6 @@ export default {
content-class="gl-display-none"
nav-class="gl-border-none!"
/>
-
- <div class="gl-w-full gl-md-w-auto gl-display-flex">
- <gl-button v-if="newRunnerPath" :href="newRunnerPath" variant="confirm">
- {{ s__('Runners|New group runner') }}
- </gl-button>
- <registration-dropdown
- v-if="registrationToken"
- class="gl-ml-3"
- :registration-token="registrationToken"
- :type="$options.GROUP_TYPE"
- placement="right"
- />
- </div>
</div>
<div
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3"
diff --git a/app/assets/javascripts/ci/runner/project_runners/index.js b/app/assets/javascripts/ci/runner/project_runners/index.js
deleted file mode 100644
index 3be2b4a7422..00000000000
--- a/app/assets/javascripts/ci/runner/project_runners/index.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import Vue from 'vue';
-import ProjectRunnersApp from './project_runners_app.vue';
-
-export const initProjectRunners = (selector = '#js-project-runners') => {
- const el = document.querySelector(selector);
-
- if (!el) {
- return null;
- }
-
- const { projectFullPath } = el.dataset;
-
- return new Vue({
- el,
- render(h) {
- return h(ProjectRunnersApp, {
- props: {
- projectFullPath,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue b/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue
deleted file mode 100644
index c7bf5e521a1..00000000000
--- a/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue
+++ /dev/null
@@ -1,19 +0,0 @@
-<script>
-export default {
- props: {
- projectFullPath: {
- required: true,
- type: String,
- },
- },
-};
-</script>
-<template>
- <div>
- <!--
- Under development
- Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/33803
- Feature rollout: https://gitlab.com/gitlab-org/gitlab/-/issues/386573
- -->
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index 3dc99baa329..8915198350f 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -97,7 +97,8 @@ const outdatedStatusParams = (status) => {
[PARAM_KEY_PAUSED]: ['false'],
[PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop!
};
- } else if (status === STATUS_PAUSED) {
+ }
+ if (status === STATUS_PAUSED) {
return {
[PARAM_KEY_PAUSED]: ['true'],
[PARAM_KEY_STATUS]: [], // Important! clear PARAM_KEY_STATUS to avoid a redirection loop!
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/ci/utils.js
index a4e695518f1..eb9e9538b75 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/ci/utils.js
@@ -1,18 +1,5 @@
import * as Sentry from '@sentry/browser';
-/**
- * capture anything starting with http:// or https://
- * https?:\/\/
- *
- * up until a disallowed character or whitespace
- * [^"<>()\\^`{|}\s]+
- *
- * and a disallowed character or whitespace, including non-ending chars .,:;!?
- * [^"<>()\\^`{|}\s.,:;!?]
- */
-export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
-export default { linkRegex };
-
export const reportToSentry = (component, failureType) => {
Sentry.withScope((scope) => {
scope.setTag('component', component);
diff --git a/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue b/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue
index fdf720a5f94..cbb9c31b54b 100644
--- a/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue
+++ b/app/assets/javascripts/ci_secure_files/components/metadata/modal.vue
@@ -101,10 +101,12 @@ export default {
if (this.fileExtension === 'cer') {
this.track('load_secure_file_metadata_cer');
return this.cerItems();
- } else if (this.fileExtension === 'p12') {
+ }
+ if (this.fileExtension === 'p12') {
this.track('load_secure_file_metadata_p12');
return this.p12Items();
- } else if (this.fileExtension === 'mobileprovision') {
+ }
+ if (this.fileExtension === 'mobileprovision') {
this.track('load_secure_file_metadata_mobileprovision');
return this.mobileprovisionItems(this.metadata);
}
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 529be7169db..7482aaca36e 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -191,9 +191,11 @@ export default {
getVersionPopoverTitle(agent) {
if (this.isVersionMismatch(agent) && this.isVersionOutdated(agent)) {
return this.$options.i18n.versionMismatchOutdatedTitle;
- } else if (this.isVersionMismatch(agent)) {
+ }
+ if (this.isVersionMismatch(agent)) {
return this.$options.i18n.versionMismatchTitle;
- } else if (this.isVersionOutdated(agent)) {
+ }
+ if (this.isVersionOutdated(agent)) {
return this.$options.i18n.versionOutdatedTitle;
}
diff --git a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
index 75850cbb108..96ecbe9fa12 100644
--- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
+++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
@@ -30,7 +30,8 @@ export default {
dropdownText() {
if (this.isRegistering) {
return this.$options.i18n.registeringAgent;
- } else if (this.selectedAgent === null) {
+ }
+ if (this.selectedAgent === null) {
return this.$options.i18n.selectAgent;
}
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 590fdb947b3..0258d8e0da0 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -125,9 +125,11 @@ export default {
k8sQuantityToGb(quantity) {
if (!quantity) {
return 0;
- } else if (quantity.endsWith(__('Ki'))) {
+ }
+ if (quantity.endsWith(__('Ki'))) {
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 0.000001024;
- } else if (quantity.endsWith(__('Mi'))) {
+ }
+ if (quantity.endsWith(__('Mi'))) {
return parseInt(quantity.substr(0, quantity.length - 2), 10) * 0.001048576;
}
@@ -138,9 +140,11 @@ export default {
k8sQuantityToCpu(quantity) {
if (!quantity) {
return 0;
- } else if (quantity.endsWith('m')) {
+ }
+ if (quantity.endsWith('m')) {
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000.0;
- } else if (quantity.endsWith('n')) {
+ }
+ if (quantity.endsWith('n')) {
return parseInt(quantity.substr(0, quantity.length - 1), 10) / 1000000000.0;
}
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
index c388d3fee71..e92b98946d0 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_actions.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -37,7 +37,8 @@ export default {
if (!this.displayClusterAgents) {
return connectClusterDeprecated;
- } else if (!this.certificateBasedClustersEnabled) {
+ }
+ if (!this.certificateBasedClustersEnabled) {
return connectCluster;
}
return connectWithAgent;
diff --git a/app/assets/javascripts/commit/constants.js b/app/assets/javascripts/commit/constants.js
index 4f865e99e46..e28009ab996 100644
--- a/app/assets/javascripts/commit/constants.js
+++ b/app/assets/javascripts/commit/constants.js
@@ -80,14 +80,14 @@ export const typeConfig = {
keyNamespace: 'gpgKeyPrimaryKeyid',
helpLink: {
label: __('Learn about signing commits'),
- path: 'user/project/repository/gpg_signed_commits/index.md',
+ path: 'user/project/repository/signed_commits/index.md',
},
},
[signatureTypes.X509]: {
keyLabel: '',
helpLink: {
label: __('Learn more about X.509 signed commits'),
- path: '/user/project/repository/x509_signed_commits/index.md',
+ path: '/user/project/repository/signed_commits/x509.md',
},
subjectTitle: __('Certificate Subject'),
issuerTitle: __('Certificate Issuer'),
@@ -98,7 +98,7 @@ export const typeConfig = {
keyNamespace: 'keyFingerprintSha256',
helpLink: {
label: __('Learn about signing commits with SSH keys.'),
- path: '/user/project/repository/ssh_signed_commits/index.md',
+ path: '/user/project/repository/signed_commits/ssh.md',
},
},
};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
index 8c8293eb09e..5e84dcbe48e 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
@@ -2,12 +2,12 @@
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getParameterByName } from '~/lib/utils/url_utility';
-import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
-import { PipelineKeyOptions } from '~/pipelines/constants';
-import eventHub from '~/pipelines/event_hub';
-import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
-import PipelinesService from '~/pipelines/services/pipelines_service';
-import PipelineStore from '~/pipelines/stores/pipelines_store';
+import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import { PipelineKeyOptions } from '~/ci/constants';
+import eventHub from '~/ci/event_hub';
+import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
+import PipelinesService from '~/ci/pipelines_page/services/pipelines_service';
+import PipelineStore from '~/ci/pipeline_details/stores/pipelines_store';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 96c274225d8..beeb9b9ada4 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -26,7 +26,8 @@ export default () => {
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
const table = new Vue({
components: {
- CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
+ CommitPipelinesTable: () =>
+ import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue'),
},
apolloProvider,
provide: {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue b/app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_table_wrapper.vue
+++ /dev/null
diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js
new file mode 100644
index 00000000000..4a9f79460da
--- /dev/null
+++ b/app/assets/javascripts/commons/gitlab_ui.js
@@ -0,0 +1,10 @@
+import applyGitLabUIConfig from '@gitlab/ui/dist/config';
+import { __ } from '~/locale';
+
+applyGitLabUIConfig({
+ translations: {
+ 'GlSearchBoxByType.input.placeholder': __('Search'),
+ 'GlSearchBoxByType.clearButtonTitle': __('Clear'),
+ 'ClearIconButton.title': __('Clear'),
+ },
+});
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index 77c85d85e27..d2a5ef83faf 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -1,6 +1,7 @@
import './polyfills';
import './bootstrap';
import './vue';
+import './gitlab_ui';
import '../lib/utils/axios_utils';
import { openUserCountsBroadcast } from './nav/user_merge_requests';
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
index 7915cd6679d..fe9b2f81f85 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_created.vue
@@ -24,7 +24,7 @@ export default {
return this.event.resource_parent;
},
message() {
- if (!this.target) {
+ if (!this.target.type) {
return EVENT_CREATED_I18N[this.resourceParent.type] || EVENT_CREATED_I18N[TYPE_FALLBACK];
}
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue
new file mode 100644
index 00000000000..11b6affb944
--- /dev/null
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_destroyed.vue
@@ -0,0 +1,28 @@
+<script>
+import { EVENT_DESTROYED_I18N, EVENT_DESTROYED_ICONS } from '../../constants';
+import { getValueByEventTarget } from '../../utils';
+import ContributionEventBase from './contribution_event_base.vue';
+
+export default {
+ name: 'ContributionEventDestroyed',
+ components: { ContributionEventBase },
+ props: {
+ event: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return getValueByEventTarget(EVENT_DESTROYED_I18N, this.event);
+ },
+ iconName() {
+ return getValueByEventTarget(EVENT_DESTROYED_ICONS, this.event);
+ },
+ },
+};
+</script>
+
+<template>
+ <contribution-event-base :event="event" :message="message" :icon-name="iconName" />
+</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue
index 557f2912f17..a2516f37c92 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_pushed.vue
@@ -53,7 +53,8 @@ export default {
message() {
if (this.ref.is_new) {
return this.$options.i18n.new[this.ref.type];
- } else if (this.ref.is_removed) {
+ }
+ if (this.ref.is_removed) {
return this.$options.i18n.removed[this.ref.type];
}
diff --git a/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue
new file mode 100644
index 00000000000..e795e611ee5
--- /dev/null
+++ b/app/assets/javascripts/contribution_events/components/contribution_event/contribution_event_updated.vue
@@ -0,0 +1,25 @@
+<script>
+import { EVENT_UPDATED_I18N } from '../../constants';
+import { getValueByEventTarget } from '../../utils';
+import ContributionEventBase from './contribution_event_base.vue';
+
+export default {
+ name: 'ContributionEventUpdated',
+ components: { ContributionEventBase },
+ props: {
+ event: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return getValueByEventTarget(EVENT_UPDATED_I18N, this.event);
+ },
+ },
+};
+</script>
+
+<template>
+ <contribution-event-base :event="event" :message="message" icon-name="pencil" />
+</template>
diff --git a/app/assets/javascripts/contribution_events/components/contribution_events.vue b/app/assets/javascripts/contribution_events/components/contribution_events.vue
index 8b42d77675f..f3116717783 100644
--- a/app/assets/javascripts/contribution_events/components/contribution_events.vue
+++ b/app/assets/javascripts/contribution_events/components/contribution_events.vue
@@ -12,6 +12,8 @@ import {
EVENT_TYPE_CLOSED,
EVENT_TYPE_REOPENED,
EVENT_TYPE_COMMENTED,
+ EVENT_TYPE_UPDATED,
+ EVENT_TYPE_DESTROYED,
} from '../constants';
import ContributionEventApproved from './contribution_event/contribution_event_approved.vue';
import ContributionEventExpired from './contribution_event/contribution_event_expired.vue';
@@ -24,6 +26,8 @@ import ContributionEventCreated from './contribution_event/contribution_event_cr
import ContributionEventClosed from './contribution_event/contribution_event_closed.vue';
import ContributionEventReopened from './contribution_event/contribution_event_reopened.vue';
import ContributionEventCommented from './contribution_event/contribution_event_commented.vue';
+import ContributionEventUpdated from './contribution_event/contribution_event_updated.vue';
+import ContributionEventDestroyed from './contribution_event/contribution_event_destroyed.vue';
export default {
props: {
@@ -151,6 +155,12 @@ export default {
case EVENT_TYPE_COMMENTED:
return ContributionEventCommented;
+ case EVENT_TYPE_UPDATED:
+ return ContributionEventUpdated;
+
+ case EVENT_TYPE_DESTROYED:
+ return ContributionEventDestroyed;
+
default:
return EmptyComponent;
}
diff --git a/app/assets/javascripts/contribution_events/components/target_link.vue b/app/assets/javascripts/contribution_events/components/target_link.vue
index a14574ed826..86fddbb063e 100644
--- a/app/assets/javascripts/contribution_events/components/target_link.vue
+++ b/app/assets/javascripts/contribution_events/components/target_link.vue
@@ -27,5 +27,5 @@ export default {
</script>
<template>
- <gl-link v-if="target" v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link>
+ <gl-link v-if="target.type" v-bind="targetLinkAttributes">{{ targetLinkText }}</gl-link>
</template>
diff --git a/app/assets/javascripts/contribution_events/constants.js b/app/assets/javascripts/contribution_events/constants.js
index b5eddbf7e25..c0224ce94ff 100644
--- a/app/assets/javascripts/contribution_events/constants.js
+++ b/app/assets/javascripts/contribution_events/constants.js
@@ -119,14 +119,31 @@ export const EVENT_COMMENTED_I18N = Object.freeze({
[COMMIT_NOTEABLE_TYPE]: s__(
'ContributionEvent|Commented on commit %{noteableLink} in %{resourceParentLink}.',
),
- fallback: s__('ContributionEvent|Commented on %{noteableLink}.'),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Commented on %{noteableLink}.'),
});
export const EVENT_COMMENTED_SNIPPET_I18N = Object.freeze({
[RESOURCE_PARENT_TYPE_PROJECT]: s__(
'ContributionEvent|Commented on snippet %{noteableLink} in %{resourceParentLink}.',
),
- fallback: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Commented on snippet %{noteableLink}.'),
+});
+
+export const EVENT_UPDATED_I18N = Object.freeze({
+ [TARGET_TYPE_DESIGN]: s__(
+ 'ContributionEvent|Updated design %{targetLink} in %{resourceParentLink}.',
+ ),
+ [TARGET_TYPE_WIKI]: s__(
+ 'ContributionEvent|Updated wiki page %{targetLink} in %{resourceParentLink}.',
+ ),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Updated resource.'),
+});
+
+export const EVENT_DESTROYED_I18N = Object.freeze({
+ [TARGET_TYPE_DESIGN]: s__('ContributionEvent|Archived design in %{resourceParentLink}.'),
+ [TARGET_TYPE_WIKI]: s__('ContributionEvent|Deleted wiki page in %{resourceParentLink}.'),
+ [TARGET_TYPE_MILESTONE]: s__('ContributionEvent|Deleted milestone in %{resourceParentLink}.'),
+ [TYPE_FALLBACK]: s__('ContributionEvent|Deleted resource.'),
});
export const EVENT_CLOSED_ICONS = Object.freeze({
@@ -139,3 +156,8 @@ export const EVENT_REOPENED_ICONS = Object.freeze({
[TARGET_TYPE_MERGE_REQUEST]: 'merge-request-open',
[TYPE_FALLBACK]: 'status_open',
});
+
+export const EVENT_DESTROYED_ICONS = Object.freeze({
+ [TARGET_TYPE_DESIGN]: 'archive',
+ [TYPE_FALLBACK]: 'remove',
+});
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index b39720c6094..08c2e235819 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -114,4 +114,8 @@ export default class CreateItemDropdown {
toggleFooter(toggleState) {
this.$dropdownFooter.toggleClass('hidden', toggleState);
}
+
+ close() {
+ this.$dropdown.data('deprecatedJQueryDropdown')?.close();
+ }
}
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
index a56fce98f85..ccf4b064fa4 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -249,7 +249,12 @@ export default {
:description="$options.translations.addTokenNameDescription"
label-for="deploy_token_name"
>
- <gl-form-input id="deploy_token_name" v-model="name" name="deploy_token_name" />
+ <gl-form-input
+ id="deploy_token_name"
+ v-model="name"
+ class="gl-form-input-xl"
+ name="deploy_token_name"
+ />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenExpiryLabel"
@@ -258,6 +263,7 @@ export default {
>
<gl-form-input
id="deploy_token_expires_at"
+ class="gl-form-input-xl"
name="deploy_token_expires_at"
:value="formattedExpiryDate"
data-qa-selector="deploy_token_expires_at_field"
@@ -277,7 +283,7 @@ export default {
</template>
</gl-sprintf>
</template>
- <gl-form-input id="deploy_token_username" v-model="username" />
+ <gl-form-input id="deploy_token_username" v-model="username" class="gl-form-input-xl" />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenScopesLabel"
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
index 69f1d62539a..3f14a8a8a26 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown.js
@@ -94,6 +94,7 @@ export class GitLabDropdown {
dataType: this.options.dataType,
beforeSend: this.toggleLoading.bind(this),
success: (data) => {
+ this.dropdown.trigger('done.remote.loading.gl.dropdown');
this.fullData = data;
this.parseData(this.fullData);
this.focusTextInput();
@@ -220,7 +221,12 @@ export class GitLabDropdown {
}
toggleLoading() {
- return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+ const menu = this.dropdown[0].querySelector('.dropdown-menu');
+ const isLoading = menu.classList.contains(LOADING_CLASS);
+
+ this.dropdown.trigger(`toggle.${isLoading ? 'off' : 'on'}.loading.gl.dropdown`);
+
+ menu.classList.toggle(LOADING_CLASS);
}
togglePage() {
@@ -719,4 +725,8 @@ export class GitLabDropdown {
clearField(field, isInput) {
return isInput ? field.val('') : field.remove();
}
+
+ close() {
+ this.dropdown.find('.dropdown-menu-toggle').dropdown('hide');
+ }
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
index 2cb9e9a56a3..271347feebd 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_filter.js
@@ -17,9 +17,11 @@ export class GitLabDropdownFilter {
const $inputContainer = this.input.parent();
const $clearButton = $inputContainer.find('.js-dropdown-input-clear');
const filterRemoteDebounced = debounce(() => {
+ options.instance.dropdown.trigger('filtering.gl.dropdown');
$inputContainer.parent().addClass('is-loading');
return this.options.query(this.input.val(), (data) => {
+ options.instance.dropdown.trigger('done.filtering.gl.dropdown');
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
});
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
index ae5d3298b62..bacd84048af 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/gl_dropdown_remote.js
@@ -11,7 +11,8 @@ export class GitLabDropdownRemote {
execute() {
if (typeof this.dataEndpoint === 'string') {
return this.fetchData();
- } else if (typeof this.dataEndpoint === 'function') {
+ }
+ if (typeof this.dataEndpoint === 'function') {
if (this.options.beforeSend) {
this.options.beforeSend();
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/index.js b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
index 6a3d2026192..38236707e06 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/index.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/index.js
@@ -5,7 +5,10 @@ export default function initDeprecatedJQueryDropdown($el, opts) {
// eslint-disable-next-line func-names
return $el.each(function () {
if (!$.data(this, 'deprecatedJQueryDropdown')) {
- $.data(this, 'deprecatedJQueryDropdown', new GitLabDropdown(this, opts));
+ const instance = new GitLabDropdown(this, opts);
+
+ $.data(this, 'deprecatedJQueryDropdown', instance);
+ this.GitLabDropdownInstance = instance;
}
});
}
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 97698d55011..1b133c57302 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -89,7 +89,8 @@ function checkSelected(data, options) {
if (!options.parent) {
return !data.id;
- } else if (value) {
+ }
+ if (value) {
return (
options.parent.querySelector(`input[name='${options.fieldName}'][value='${value}']`) != null
);
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 4e5e07c57e4..008e12abbcd 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -458,7 +458,8 @@ export default class Notes {
this.setupNewNote($newNote);
this.refresh();
return this.updateNotesCount(1);
- } else if (Notes.isUpdatedNote(noteEntity, $note)) {
+ }
+ if (Notes.isUpdatedNote(noteEntity, $note)) {
// The server can send the same update multiple times so we need to make sure to only update once per actual update.
const isEditing = $note.hasClass('is-editing');
const initialContent = normalizeNewlines($note.find('.original-note-content').text().trim());
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index e3882ce42c2..08306312c2e 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -19,7 +19,6 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import { Mousetrap } from '~/lib/mousetrap';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '~/notes/event_hub';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
@@ -76,7 +75,6 @@ export default {
GenerateTestFileDrawer: () =>
import('ee_component/ai/components/generate_test_file_drawer.vue'),
},
- mixins: [glFeatureFlagsMixin()],
alerts: {
ALERT_OVERFLOW_HIDDEN,
ALERT_MERGE_CONFLICT,
@@ -580,11 +578,7 @@ export default {
<template>
<div v-show="shouldShow">
- <findings-drawer
- v-if="glFeatures.codeQualityInlineDrawer"
- :drawer="activeDrawer"
- @close="closeDrawer"
- />
+ <findings-drawer :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" />
@@ -631,6 +625,7 @@ export default {
<template #default="{ item, index, active }">
<dynamic-scroller-item :item="item" :active="active" :class="{ active }">
<diff-file
+ v-if="active"
:file="item"
:reviewed="fileReviews[item.id]"
:is-first-file="index === 0"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index d62d0e11bff..a9e63ad53bb 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -337,7 +337,7 @@ export default {
:title="filePath"
class="file-title-name"
data-container="body"
- data-qa-selector="file_name_content"
+ data-testid="file-name-content"
>
{{ filePath }}
</strong>
diff --git a/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue b/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue
index 5cc2a3079b0..8a30d6ee7f8 100644
--- a/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue
+++ b/app/assets/javascripts/diffs/components/diff_inline_findings_item.vue
@@ -1,23 +1,15 @@
<script>
-import { GlLink, GlIcon } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions } from 'vuex';
+import { GlIcon } from '@gitlab/ui';
import { getSeverity } from '~/ci/reports/utils';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- components: { GlLink, GlIcon },
- mixins: [glFeatureFlagsMixin()],
+ components: { GlIcon },
+
props: {
finding: {
type: Object,
required: true,
},
- link: {
- type: Boolean,
- required: false,
- default: true,
- },
},
computed: {
enhancedFinding() {
@@ -27,12 +19,6 @@ export default {
return `${this.finding.severity} - ${this.finding.description}`;
},
},
- methods: {
- toggleDrawer() {
- this.setDrawer(this.finding);
- },
- ...mapActions('findingsDrawer', ['setDrawer']),
- },
};
</script>
@@ -46,17 +32,7 @@ export default {
class="inline-findings-severity-icon"
/>
</span>
- <span
- v-if="glFeatures.codeQualityInlineDrawer"
- data-testid="description-button-section"
- class="gl-display-flex"
- >
- <gl-link v-if="link" category="primary" variant="link" @click="toggleDrawer">
- {{ listText }}</gl-link
- >
- <span v-else>{{ listText }}</span>
- </span>
- <span v-else data-testid="description-plain-text" class="gl-display-flex">
+ <span data-testid="description-plain-text" class="gl-display-flex">
{{ listText }}
</span>
</li>
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 318ecc89d14..ee6e9a2fc94 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -24,8 +24,8 @@ import * as utils from './diff_row_utils';
export default {
DiffGutterAvatars,
- InlineFindingsGutterIcon: () =>
- import('ee_component/diffs/components/inline_findings_gutter_icon.vue'),
+ InlineFindingsFlagSwitcher: () =>
+ import('ee_component/diffs/components/inline_findings_flag_switcher.vue'),
// Temporary mixin for migration from Vue.js 2 to @vue/compat
mixins: [compatFunctionalMixin],
@@ -338,10 +338,11 @@ export default {
:class="$options.parallelViewLeftLineType(props)"
>
<component
- :is="$options.InlineFindingsGutterIcon"
+ :is="$options.InlineFindingsFlagSwitcher"
v-if="$options.showCodequalityLeft(props) || $options.showSecurityLeft(props)"
:inline-findings-expanded="props.inlineFindingsExpanded"
- :codequality="props.line.left.codequality"
+ :code-quality="props.line.left.codequality"
+ :sast="props.line.left.sast"
:file-path="props.filePath"
@showInlineFindings="
listeners.toggleCodeQualityFindings(
@@ -479,9 +480,10 @@ export default {
:class="$options.classNameMapCellRight(props)"
>
<component
- :is="$options.InlineFindingsGutterIcon"
+ :is="$options.InlineFindingsFlagSwitcher"
v-if="$options.showCodequalityRight(props) || $options.showSecurityRight(props)"
- :codequality="props.line.right.codequality"
+ :code-quality="props.line.right.codequality"
+ :sast="props.line.right.sast"
:file-path="props.filePath"
data-testid="inlineFindingsIcon"
@showInlineFindings="
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 88ea4e15552..641616a34f5 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -186,7 +186,8 @@ export default {
getCountBetweenIndex(index) {
if (index === 0) {
return -1;
- } else if (!this.diffLines[index + 1]) {
+ }
+ if (!this.diffLines[index + 1]) {
return -1;
}
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index bbc602aedf6..7c68b5f69f1 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -910,7 +910,8 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
export function changeCurrentCommit({ dispatch, commit, state }, { commitId }) {
if (!commitId) {
return Promise.reject(new Error('`commitId` is a required argument'));
- } else if (!state.commit) {
+ }
+ if (!state.commit) {
return Promise.reject(new Error('`state` must already contain a valid `commit`')); // eslint-disable-line @gitlab/require-i18n-strings
}
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index d82959daa9d..bfafb4d281d 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -139,7 +139,8 @@ export const fileLineCoverage = (state) => (file, line) => {
if (lineCoverage === 0) {
return { text: __('No test coverage'), class: 'no-coverage' };
- } else if (lineCoverage >= 0) {
+ }
+ if (lineCoverage >= 0) {
return {
text: n__('Test coverage: %d hit', 'Test coverage: %d hits', lineCoverage),
class: 'coverage',
diff --git a/app/assets/javascripts/drawio/drawio_editor.js b/app/assets/javascripts/drawio/drawio_editor.js
index 3c411d8093c..37bfde0ed9f 100644
--- a/app/assets/javascripts/drawio/drawio_editor.js
+++ b/app/assets/javascripts/drawio/drawio_editor.js
@@ -1,6 +1,7 @@
import _ from 'lodash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { base64DecodeUnicode } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
import { setAttributes } from '~/lib/utils/dom_utils';
import {
@@ -28,7 +29,7 @@ function disposeDrawioEditor(drawIOEditorState) {
}
function getSvg(data) {
- const svgPath = atob(data.substring(data.indexOf(',') + 1));
+ const svgPath = base64DecodeUnicode(data.substring(data.indexOf(',') + 1));
return `<?xml version="1.0" encoding="UTF-8"?>\n\
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n\
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 65091487c93..2dba919cf58 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -845,6 +845,9 @@
"if": {
"$ref": "#/definitions/if"
},
+ "changes": {
+ "$ref": "#/definitions/changes"
+ },
"exists": {
"$ref": "#/definitions/exists"
},
@@ -2084,6 +2087,10 @@
"publish": {
"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"
}
},
"oneOf": [
diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js
new file mode 100644
index 00000000000..8eb265cb1e8
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/analytics.js
@@ -0,0 +1,17 @@
+import { glClientSDK } from '@gitlab/application-sdk-browser';
+
+const { analytics_id: appId, analytics_url: host } = window.gon;
+
+if (appId && host) {
+ window.glClient = glClientSDK({
+ appId,
+ host,
+ hasCookieConsent: true,
+ plugins: {
+ clientHints: false,
+ linkTracking: false,
+ performanceTiming: false,
+ errorTracking: false,
+ },
+ });
+}
diff --git a/app/assets/javascripts/entrypoints/jira_connect_app.js b/app/assets/javascripts/entrypoints/jira_connect_app.js
new file mode 100644
index 00000000000..90ad39ea487
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/jira_connect_app.js
@@ -0,0 +1 @@
+import '../jira_connect/subscriptions';
diff --git a/app/assets/javascripts/entrypoints/main.js b/app/assets/javascripts/entrypoints/main.js
new file mode 100644
index 00000000000..6d59e89cfd0
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main.js
@@ -0,0 +1,6 @@
+import '../main';
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../pages/**/index.js');
+
+runModules(modules, '../pages/');
diff --git a/app/assets/javascripts/entrypoints/main_ee.js b/app/assets/javascripts/entrypoints/main_ee.js
new file mode 100644
index 00000000000..4a83be6be94
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main_ee.js
@@ -0,0 +1,5 @@
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../../../../ee/app/assets/javascripts/pages/**/index.js');
+
+runModules(modules, '../../../../ee/app/assets/javascripts/pages/');
diff --git a/app/assets/javascripts/entrypoints/main_jh.js b/app/assets/javascripts/entrypoints/main_jh.js
new file mode 100644
index 00000000000..92a42a9ac70
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/main_jh.js
@@ -0,0 +1,5 @@
+import { runModules } from '~/run_modules';
+
+const modules = import.meta.glob('../../../../jh/app/assets/javascripts/pages/**/index.js');
+
+runModules(modules, '../../../../jh/app/assets/javascripts/pages/');
diff --git a/app/assets/javascripts/entrypoints/performance_bar.js b/app/assets/javascripts/entrypoints/performance_bar.js
new file mode 100644
index 00000000000..3f6fc6272d0
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/performance_bar.js
@@ -0,0 +1 @@
+import '../performance_bar';
diff --git a/app/assets/javascripts/entrypoints/redirect_listbox.js b/app/assets/javascripts/entrypoints/redirect_listbox.js
new file mode 100644
index 00000000000..811a73fbf2f
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/redirect_listbox.js
@@ -0,0 +1 @@
+import './behaviors/redirect_listbox';
diff --git a/app/assets/javascripts/entrypoints/sandboxed_mermaid.js b/app/assets/javascripts/entrypoints/sandboxed_mermaid.js
new file mode 100644
index 00000000000..d3dd144ffba
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sandboxed_mermaid.js
@@ -0,0 +1 @@
+import '../lib/mermaid';
diff --git a/app/assets/javascripts/entrypoints/sentry.js b/app/assets/javascripts/entrypoints/sentry.js
new file mode 100644
index 00000000000..debafc6fab3
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sentry.js
@@ -0,0 +1 @@
+import '../sentry/index';
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index 96d2a8d9ba2..ef6d3b79198 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -209,7 +209,7 @@ export default {
</div>
<time-ago-tooltip v-if="createdAt" :time="createdAt" class="gl-display-flex">
<template #default="{ timeAgo }">
- <gl-icon name="calendar" />
+ <gl-icon name="calendar" class="gl-mr-2" />
<span class="gl-mr-2 gl-white-space-nowrap">{{ timeAgo }}</span>
</template>
</time-ago-tooltip>
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index f90a1dcd193..0eebd81b671 100644
--- a/app/assets/javascripts/environments/components/edit_environment.vue
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -4,7 +4,6 @@ import { createAlert } from '~/alert';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getEnvironment from '../graphql/queries/environment.query.graphql';
-import getEnvironmentWithFluxResource from '../graphql/queries/environment_with_flux_resource.query.graphql';
import updateEnvironment from '../graphql/mutations/update_environment.mutation.graphql';
import EnvironmentForm from './environment_form.vue';
@@ -17,11 +16,7 @@ export default {
inject: ['projectEnvironmentsPath', 'projectPath', 'environmentName'],
apollo: {
environment: {
- query() {
- return this.glFeatures?.fluxResourceForEnvironment
- ? getEnvironmentWithFluxResource
- : getEnvironment;
- },
+ query: getEnvironment,
variables() {
return {
environmentName: this.environmentName,
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index d49598d2f21..926c556966c 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -87,7 +87,7 @@ export default {
</script>
<template>
<gl-disclosure-dropdown
- :text="title"
+ :toggle-text="title"
:title="title"
:loading="isLoading"
:aria-label="title"
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index d89dcf56b7c..846f2cf73b2 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -181,13 +181,8 @@ export default {
namespaceDropdownToggleText() {
return this.selectedNamespace || this.$options.i18n.namespaceHelpText;
},
- isKasFluxResourceAvailable() {
- return this.glFeatures?.fluxResourceForEnvironment;
- },
showFluxResourceSelector() {
- return Boolean(
- this.isKasFluxResourceAvailable && this.selectedNamespace && this.selectedAgentId,
- );
+ return Boolean(this.selectedNamespace && this.selectedAgentId);
},
k8sAccessConfiguration() {
if (!this.showNamespaceSelector) {
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index b02142c24cf..08a1eacec7a 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,6 +1,6 @@
<script>
import {
- GlDropdown,
+ GlDisclosureDropdown,
GlTooltipDirective,
GlIcon,
GlLink,
@@ -24,7 +24,6 @@ import PinComponent from './environment_pin.vue';
import RollbackComponent from './environment_rollback.vue';
import StopComponent from './environment_stop.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
-
/**
* Environment Item Component
*
@@ -36,7 +35,7 @@ export default {
ActionsComponent,
CommitComponent,
ExternalUrlComponent,
- GlDropdown,
+ GlDisclosureDropdown,
GlBadge,
GlIcon,
GlLink,
@@ -820,13 +819,13 @@ export default {
data-track-label="environment_stop"
/>
- <gl-dropdown
- v-if="hasExtraActions"
- icon="ellipsis_v"
+ <gl-disclosure-dropdown
text-sr-only
- :text="__('More actions')"
- category="secondary"
no-caret
+ icon="ellipsis_v"
+ category="secondary"
+ placement="right"
+ :toggle-text="__('More actions')"
>
<rollback-component
v-if="canRetry"
@@ -857,7 +856,7 @@ export default {
data-track-action="click_button"
data-track-label="environment_delete"
/>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index e8857dfe459..c603d83db9c 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -145,15 +145,20 @@ export default {
syncStatusBadge() {
if (!this.fluxCRD.length && this.fluxApiError) {
return { ...SYNC_STATUS_BADGES.unavailable, popoverText: this.fluxApiError };
- } else if (!this.fluxCRD.length) {
+ }
+ if (!this.fluxCRD.length) {
return SYNC_STATUS_BADGES.unavailable;
- } else if (this.fluxAnyFailed) {
+ }
+ if (this.fluxAnyFailed) {
return { ...SYNC_STATUS_BADGES.failed, popoverText: this.fluxAnyFailed.message };
- } else if (this.fluxAnyStalled) {
+ }
+ if (this.fluxAnyStalled) {
return { ...SYNC_STATUS_BADGES.stalled, popoverText: this.fluxAnyStalled.message };
- } else if (this.fluxAnyReconciling) {
+ }
+ if (this.fluxAnyReconciling) {
return SYNC_STATUS_BADGES.reconciling;
- } else if (this.fluxAnyReconciled) {
+ }
+ if (this.fluxAnyReconciled) {
return SYNC_STATUS_BADGES.reconciled;
}
return SYNC_STATUS_BADGES.unknown;
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index 2148343f690..149cab21acd 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -14,7 +14,6 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
import getEnvironmentClusterAgent from '../graphql/queries/environment_cluster_agent.query.graphql';
-import getEnvironmentClusterAgentWithFluxResource from '../graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
import StopComponent from './environment_stop.vue';
@@ -165,9 +164,6 @@ export default {
rolloutStatus() {
return this.environment?.rolloutStatus;
},
- isFluxResourceAvailable() {
- return this.glFeatures?.fluxResourceForEnvironment;
- },
},
methods: {
toggleEnvironmentCollapse() {
@@ -185,9 +181,7 @@ export default {
return { environmentName: this.environment.name, projectFullPath: this.projectPath };
},
query() {
- return this.isFluxResourceAvailable
- ? getEnvironmentClusterAgentWithFluxResource
- : getEnvironmentClusterAgent;
+ return getEnvironmentClusterAgent;
},
update(data) {
this.clusterAgent = data?.project?.environment?.clusterAgent;
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
index f37f93798ae..261d8106438 100644
--- a/app/assets/javascripts/environments/environment_details/deployments_table.vue
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -36,7 +36,7 @@ export default {
<deployment-status-link :deployment-job="item.job" :status="item.status" />
</template>
<template #cell(id)="{ item }">
- <strong>{{ item.id }}</strong>
+ <strong data-testid="deployment-id">{{ item.id }}</strong>
</template>
<template #cell(triggerer)="{ item }">
<deployment-triggerer :triggerer="item.triggerer" />
diff --git a/app/assets/javascripts/environments/graphql/queries/environment.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment.query.graphql
index 53dfe5303f3..2d6faed5c88 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment.query.graphql
@@ -6,6 +6,7 @@ query getEnvironment($projectFullPath: ID!, $environmentName: String) {
name
externalUrl
kubernetesNamespace
+ fluxResourcePath
clusterAgent {
id
name
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
index 19374ae7a81..3f8874f2a8d 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent.query.graphql
@@ -3,6 +3,7 @@ query getEnvironmentClusterAgent($projectFullPath: ID!, $environmentName: String
id
environment(name: $environmentName) {
id
+ fluxResourcePath
kubernetesNamespace
clusterAgent {
id
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql
deleted file mode 100644
index 80363a06d42..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/environment_cluster_agent_with_flux_resource.query.graphql
+++ /dev/null
@@ -1,21 +0,0 @@
-query getEnvironmentClusterAgentWithFluxResource($projectFullPath: ID!, $environmentName: String) {
- project(fullPath: $projectFullPath) {
- id
- environment(name: $environmentName) {
- id
- kubernetesNamespace
- fluxResourcePath
- clusterAgent {
- id
- name
- webPath
- tokens {
- nodes {
- id
- lastUsedAt
- }
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql
deleted file mode 100644
index 166cd64189f..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/environment_with_flux_resource.query.graphql
+++ /dev/null
@@ -1,16 +0,0 @@
-query getEnvironmentWithFluxResource($projectFullPath: ID!, $environmentName: String) {
- project(fullPath: $projectFullPath) {
- id
- environment(name: $environmentName) {
- id
- name
- externalUrl
- kubernetesNamespace
- fluxResourcePath
- clusterAgent {
- id
- name
- }
- }
- }
-}
diff --git a/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js b/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
index 55e2536e283..a64cc0405bb 100644
--- a/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
@@ -3,7 +3,7 @@
*
* Components need to have `scope`, `page` and `requestData`
*/
-import { validateParams } from '~/pipelines/utils';
+import { validateParams } from '~/ci/pipeline_details/utils';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
export default {
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index fb9a7a02d07..9924e1c7d7b 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -59,9 +59,6 @@ export const initHeader = () => {
};
export const initPage = async () => {
- if (!gon.features.environmentDetailsVue) {
- return null;
- }
const EnvironmentsDetailPageModule = await import('./environment_details/index.vue');
const EnvironmentsDetailPage = EnvironmentsDetailPageModule.default;
const dataElement = document.getElementById('environments-detail-view');
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index 420c34a88f1..ad80ee099ad 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -1,25 +1,15 @@
<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlIcon,
- GlLoadingIcon,
- GlSearchBoxByType,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { GlCollapsibleListbox, GlButton } from '@gitlab/ui';
+import { debounce, memoize } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { __, sprintf } from '~/locale';
+import { __, n__, sprintf } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
export default {
components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlSearchBoxByType,
- GlIcon,
- GlLoadingIcon,
+ GlButton,
+ GlCollapsibleListbox,
},
inject: ['environmentsEndpoint'],
data() {
@@ -34,69 +24,96 @@ export default {
noResultsLabel: __('No matching results'),
},
computed: {
+ srOnlyResultsCount() {
+ return n__('%d environment found', '%d environments found', this.results.length);
+ },
createEnvironmentLabel() {
return sprintf(__('Create %{environment}'), { environment: this.environmentSearch });
},
+ isCreateEnvironmentShown() {
+ return !this.isLoading && this.results.length === 0 && Boolean(this.environmentSearch);
+ },
+ },
+ mounted() {
+ this.fetchEnvironments();
+ },
+ unmounted() {
+ // cancel debounce if the component is unmounted to avoid unnecessary fetches
+ this.fetchEnvironments.cancel();
+ },
+ created() {
+ this.fetch = memoize(async function fetchEnvironmentsFromApi(query) {
+ this.isLoading = true;
+ try {
+ const { data } = await axios.get(this.environmentsEndpoint, { params: { query } });
+
+ return data;
+ } catch {
+ createAlert({
+ message: __('Something went wrong on our end. Please try again.'),
+ });
+ return [];
+ } finally {
+ this.isLoading = false;
+ }
+ });
+
+ this.fetchEnvironments = debounce(function debouncedFetchEnvironments(query = '') {
+ this.fetch(query)
+ .then((data) => {
+ this.results = data.map((item) => ({ text: item, value: item }));
+ })
+ .catch(() => {
+ this.results = [];
+ });
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
+ onSelect(selected) {
+ this.$emit('add', selected[0]);
+ },
addEnvironment(newEnvironment) {
this.$emit('add', newEnvironment);
- this.environmentSearch = '';
this.results = [];
},
- fetchEnvironments: debounce(function debouncedFetchEnvironments() {
- this.isLoading = true;
- axios
- .get(this.environmentsEndpoint, { params: { query: this.environmentSearch } })
- .then(({ data }) => {
- this.results = data || [];
- })
- .catch(() => {
- createAlert({
- message: __('Something went wrong on our end. Please try again.'),
- });
- })
- .finally(() => {
- this.isLoading = false;
- });
- }, 250),
- setFocus() {
- this.$refs.searchBox.focusInput();
+ onSearch(query) {
+ this.environmentSearch = query;
+ this.fetchEnvironments(query);
},
},
};
</script>
<template>
- <gl-dropdown class="js-new-environments-dropdown" @shown="setFocus">
- <template #button-content>
- <span class="d-md-none mr-1">
- {{ $options.translations.addEnvironmentsLabel }}
- </span>
- <gl-icon class="d-none d-md-inline-flex gl-mr-1" name="plus" />
+ <gl-collapsible-listbox
+ icon="plus"
+ data-testid="new-environments-dropdown"
+ :toggle-text="$options.translations.addEnvironmentsLabel"
+ :items="results"
+ :searching="isLoading"
+ :header-text="$options.translations.addEnvironmentsLabel"
+ searchable
+ multiple
+ @search="onSearch"
+ @select="onSelect"
+ >
+ <template #footer>
+ <div
+ v-if="isCreateEnvironmentShown"
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2"
+ >
+ <gl-button
+ category="tertiary"
+ block
+ class="gl-justify-content-start!"
+ data-testid="add-environment-button"
+ @click="addEnvironment(environmentSearch)"
+ >
+ {{ createEnvironmentLabel }}
+ </gl-button>
+ </div>
</template>
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="environmentSearch"
- @focus="fetchEnvironments"
- @keyup="fetchEnvironments"
- />
- <gl-loading-icon v-if="isLoading" size="sm" />
- <gl-dropdown-item
- v-for="environment in results"
- v-else-if="results.length"
- :key="environment"
- @click="addEnvironment(environment)"
- >
- {{ environment }}
- </gl-dropdown-item>
- <template v-else-if="environmentSearch.length">
- <span ref="noResults" class="text-secondary gl-p-3">
- {{ $options.translations.noMatchingResults }}
- </span>
- <gl-dropdown-divider />
- <gl-dropdown-item @click="addEnvironment(environmentSearch)">
- {{ createEnvironmentLabel }}
- </gl-dropdown-item>
+ <template #search-summary-sr-only>
+ {{ srOnlyResultsCount }}
</template>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 684375177bb..8ccf7ba92a5 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -42,7 +42,7 @@ export default class FilteredSearchManager {
useDefaultState = false,
filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
- placeholder = __('Search or filter results...'),
+ placeholder = __('Search or filter results…'),
anchor = null,
}) {
this.isGroup = isGroup;
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
index 1c33c8b1084..f71405a5bc4 100644
--- a/app/assets/javascripts/frequent_items/utils.js
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -24,7 +24,8 @@ export const getTopFrequentItems = (items) => {
// and then by lastAccessedOn with recent most first
if (itemA.frequency !== itemB.frequency) {
return itemB.frequency - itemA.frequency;
- } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ }
+ if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
return itemB.lastAccessedOn - itemA.lastAccessedOn;
}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9e7006bb6e7..264427f5806 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -393,13 +393,16 @@ class GfmAutoComplete {
if (command === MEMBER_COMMAND.ASSIGN) {
// Only include members which are not assigned to Issuable currently
return data.filter((member) => !assignees.includes(member.search));
- } else if (command === MEMBER_COMMAND.UNASSIGN) {
+ }
+ if (command === MEMBER_COMMAND.UNASSIGN) {
// Only include members which are assigned to Issuable currently
return data.filter((member) => assignees.includes(member.search));
- } else if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
+ }
+ if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
// Only include members which are not assigned as a reviewer to Issuable currently
return data.filter((member) => !reviewers.includes(member.search));
- } else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
+ }
+ if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
// Only include members which are not assigned as a reviewer to Issuable currently
return data.filter((member) => reviewers.includes(member.search));
}
@@ -617,11 +620,6 @@ class GfmAutoComplete {
if (labels.find((label) => label.title.startsWith(lastCandidate))) {
return lastCandidate;
}
- } else {
- // Load all labels into the autocompleter.
- // This needs to happen if e.g. editing a label in an existing comment, because normally
- // label data would only be loaded only once you type `~`.
- fetchData(this.$inputor, this.at);
}
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
@@ -642,7 +640,8 @@ class GfmAutoComplete {
if (command === LABEL_COMMAND.LABEL || command === LABEL_COMMAND.LABELS) {
// Return labels with set: undefined.
return data.filter((label) => !label.set);
- } else if (command === LABEL_COMMAND.UNLABEL) {
+ }
+ if (command === LABEL_COMMAND.UNLABEL) {
// Return labels with set: true.
return data.filter((label) => label.set);
}
@@ -751,7 +750,8 @@ class GfmAutoComplete {
if (command === CONTACTS_ADD_COMMAND) {
// Return contacts that are active and not already on the issue
return data.filter((contact) => contact.state === CONTACT_STATE_ACTIVE && !contact.set);
- } else if (command === CONTACTS_REMOVE_COMMAND) {
+ }
+ if (command === CONTACTS_REMOVE_COMMAND) {
// Return contacts already on the issue
return data.filter((contact) => contact.set);
}
@@ -779,10 +779,8 @@ class GfmAutoComplete {
if (GfmAutoComplete.isLoading(data)) {
self.fetchData(this.$inputor, this.at);
return data;
- } else if (
- GfmAutoComplete.isTypeWithBackendFiltering(this.at) &&
- self.previousQuery !== query
- ) {
+ }
+ if (GfmAutoComplete.isTypeWithBackendFiltering(this.at) && self.previousQuery !== query) {
self.fetchData(this.$inputor, this.at, query);
self.previousQuery = query;
return data;
diff --git a/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue b/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
index 1536a9a525b..25ddfc911f3 100644
--- a/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
+++ b/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
@@ -30,9 +30,11 @@ export default {
title() {
if (this.status === STATUS_TYPES.SUCCESS) {
return s__('VersionCheck|Up to date');
- } else if (this.status === STATUS_TYPES.WARNING) {
+ }
+ if (this.status === STATUS_TYPES.WARNING) {
return s__('VersionCheck|Update available');
- } else if (this.status === STATUS_TYPES.DANGER) {
+ }
+ if (this.status === STATUS_TYPES.DANGER) {
return s__('VersionCheck|Update ASAP');
}
diff --git a/app/assets/javascripts/google_cloud/deployments/service_table.vue b/app/assets/javascripts/google_cloud/deployments/service_table.vue
index 26c9fd14dc6..9388b41127e 100644
--- a/app/assets/javascripts/google_cloud/deployments/service_table.vue
+++ b/app/assets/javascripts/google_cloud/deployments/service_table.vue
@@ -34,7 +34,7 @@ export default {
methods: {
actionUrl(key) {
if (key === cloudRun) return this.cloudRunUrl;
- else if (key === cloudStorage) return this.cloudStorageUrl;
+ if (key === cloudStorage) return this.cloudStorageUrl;
return '#';
},
},
diff --git a/app/assets/javascripts/google_cloud/service_accounts/list.vue b/app/assets/javascripts/google_cloud/service_accounts/list.vue
index 635b185d207..ebba9332c53 100644
--- a/app/assets/javascripts/google_cloud/service_accounts/list.vue
+++ b/app/assets/javascripts/google_cloud/service_accounts/list.vue
@@ -63,6 +63,7 @@ export default {
:primary-button-link="createUrl"
:primary-button-text="$options.i18n.createServiceAccount"
:svg-path="emptyIllustrationUrl"
+ :svg-height="150"
/>
<div v-else>
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index e6c0b86d9a6..37c1674cc5a 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -48,6 +48,10 @@
"ExternalAuditEventDestination",
"InstanceExternalAuditEventDestination"
],
+ "GoogleCloudLoggingConfigurationInterface": [
+ "GoogleCloudLoggingConfigurationType",
+ "InstanceGoogleCloudLoggingConfigurationType"
+ ],
"Issuable": [
"Epic",
"Issue",
@@ -139,6 +143,7 @@
"WorkItem"
],
"User": [
+ "AddOnUser",
"AutocompletedUser",
"MergeRequestAssignee",
"MergeRequestAuthor",
diff --git a/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql
new file mode 100644
index 00000000000..ed318ef1b8d
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql
@@ -0,0 +1,12 @@
+#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query projectAutocompleteUsersSearch($search: String!, $fullPath: ID!) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ users: autocompleteUsers(search: $search) {
+ ...User
+ ...UserAvailability
+ }
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql
new file mode 100644
index 00000000000..8155451fb7c
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql
@@ -0,0 +1,19 @@
+#import "../fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+query projectAutocompleteUsersSearchWithMRPermissions(
+ $search: String!
+ $fullPath: ID!
+ $mergeRequestId: MergeRequestID!
+) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ users: autocompleteUsers(search: $search) {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction(id: $mergeRequestId) {
+ canMerge
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
new file mode 100644
index 00000000000..470ff45f47a
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/groups_dashboard_empty_state.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ inject: ['groupsEmptyStateIllustration'],
+ i18n: {
+ title: s__('GroupsEmptyState|A group is a collection of several projects'),
+ description: s__(
+ "GroupsEmptyState|If you organize your projects under a group, it works like a folder. You can manage your group member's permissions and access to each project in the group.",
+ ),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :description="$options.i18n.description"
+ :svg-path="groupsEmptyStateIllustration"
+ />
+</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
new file mode 100644
index 00000000000..f524b769802
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/groups_explore_empty_state.vue
@@ -0,0 +1,17 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { __ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ inject: ['groupsEmptyStateIllustration'],
+ i18n: {
+ title: __('No public groups'),
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state :title="$options.i18n.title" :svg-path="groupsEmptyStateIllustration" />
+</template>
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index e71ff6d9107..2539d899865 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -13,7 +13,7 @@ import GroupsStore from './store/groups_store';
Vue.use(Translate);
-export default () => {
+export default (EmptyStateComponent) => {
const el = document.getElementById('js-groups-tree');
// eslint-disable-next-line no-new
@@ -36,16 +36,19 @@ export default () => {
components: {
GroupsApp,
},
+ provide() {
+ const { groupsEmptyStateIllustration } = dataset;
+
+ return { groupsEmptyStateIllustration };
+ },
data() {
const showSchemaMarkup = parseBoolean(dataset.showSchemaMarkup);
- const renderEmptyState = parseBoolean(dataset.renderEmptyState);
const service = new GroupsService(dataset.endpoint);
const store = new GroupsStore({ hideProjects: true, showSchemaMarkup });
return {
store,
service,
- renderEmptyState,
loading: true,
};
},
@@ -74,7 +77,9 @@ export default () => {
props: {
store: this.store,
service: this.service,
- renderEmptyState: this.renderEmptyState,
+ },
+ scopedSlots: {
+ 'empty-state': () => createElement(EmptyStateComponent),
},
});
},
diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js
deleted file mode 100644
index 35b09e9b027..00000000000
--- a/app/assets/javascripts/helpers/avatar_helper.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { escape } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { getFirstCharacterCapitalized } from '~/lib/utils/text_utility';
-
-export const DEFAULT_SIZE_CLASS = 's40';
-export const IDENTICON_BG_COUNT = 7;
-
-export function getIdenticonBackgroundClass(entityId) {
- // If a GraphQL string id is passed in, convert it to the entity number
- const id = typeof entityId === 'string' ? getIdFromGraphQLId(entityId) : entityId;
- const type = (id % IDENTICON_BG_COUNT) + 1;
- return `bg${type}`;
-}
-
-export function getIdenticonTitle(entityName) {
- return getFirstCharacterCapitalized(entityName) || ' ';
-}
-
-export function renderIdenticon(entity, options = {}) {
- const { sizeClass = DEFAULT_SIZE_CLASS } = options;
-
- const bgClass = getIdenticonBackgroundClass(entity.id);
- const title = getIdenticonTitle(entity.name);
-
- return `<div class="avatar identicon ${escape(sizeClass)} ${escape(bgClass)}">${escape(
- title,
- )}</div>`;
-}
-
-export function renderAvatar(entity, options = {}) {
- if (!entity.avatar_url) {
- return renderIdenticon(entity, options);
- }
-
- const { sizeClass = DEFAULT_SIZE_CLASS } = options;
-
- return `<img src="${escape(entity.avatar_url)}" class="avatar ${escape(sizeClass)}" />`;
-}
diff --git a/app/assets/javascripts/ide/commit_icon.js b/app/assets/javascripts/ide/commit_icon.js
index 70ee9cff22b..07eef897910 100644
--- a/app/assets/javascripts/ide/commit_icon.js
+++ b/app/assets/javascripts/ide/commit_icon.js
@@ -3,7 +3,8 @@ import { commitItemIconMap } from './constants';
export default (file) => {
if (file.deleted) {
return commitItemIconMap.deleted;
- } else if (file.tempFile && !file.prevPath) {
+ }
+ if (file.tempFile && !file.prevPath) {
return commitItemIconMap.addition;
}
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
deleted file mode 100644
index f58a35e7624..00000000000
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlIcon, GlLoadingIcon } from '@gitlab/ui';
-import $ from 'jquery';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
-import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
-
-export default {
- components: {
- DropdownButton,
- GlIcon,
- GlLoadingIcon,
- },
- props: {
- data: {
- type: Array,
- required: false,
- default: () => [],
- },
- label: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: false,
- default: null,
- },
- isAsyncData: {
- type: Boolean,
- required: false,
- default: false,
- },
- searchable: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- search: '',
- };
- },
- computed: {
- ...mapState('fileTemplates', ['templates', 'isLoading']),
- outputData() {
- return (this.isAsyncData ? this.templates : this.data).filter((t) => {
- if (!this.searchable) return true;
-
- return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
- });
- },
- showLoading() {
- return this.isAsyncData ? this.isLoading : false;
- },
- },
- mounted() {
- $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
- },
- beforeDestroy() {
- $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
- },
- methods: {
- ...mapActions('fileTemplates', ['fetchTemplateTypes']),
- fetchTemplatesIfAsync() {
- if (this.isAsyncData) {
- this.fetchTemplateTypes();
- }
- },
- clickItem(item) {
- this.$emit('click', item);
- },
- },
-};
-</script>
-
-<template>
- <div class="dropdown">
- <dropdown-button :toggle-text="label" data-display="static" />
- <div class="dropdown-menu pb-0">
- <div v-if="title" class="dropdown-title ml-0 mr-0">{{ title }}</div>
- <div v-if="!showLoading && searchable" class="dropdown-input">
- <input
- v-model="search"
- :placeholder="__('Filter...')"
- type="search"
- class="dropdown-input-field"
- />
- <gl-icon name="search" class="dropdown-input-search" />
- </div>
- <div class="dropdown-content">
- <gl-loading-icon v-if="showLoading" size="lg" />
- <ul v-else>
- <li v-for="(item, index) in outputData" :key="index">
- <button type="button" @click="clickItem(item)">{{ item.name }}</button>
- </li>
- </ul>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 854daa20628..741845e3325 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -32,7 +32,8 @@ export default {
if (this.modalType === modalTypes.tree) {
return __('Create new directory');
- } else if (this.modalType === modalTypes.rename) {
+ }
+ if (this.modalType === modalTypes.rename) {
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
@@ -43,7 +44,8 @@ export default {
if (this.modalType === modalTypes.tree) {
return __('Create directory');
- } else if (this.modalType === modalTypes.rename) {
+ }
+ if (this.modalType === modalTypes.rename) {
return entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
}
diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue
index 9e8b3d87397..b2e90a64758 100644
--- a/app/assets/javascripts/ide/components/terminal/terminal.vue
+++ b/app/assets/javascripts/ide/components/terminal/terminal.vue
@@ -37,7 +37,8 @@ export default {
loadingText() {
if (isStartingStatus(this.status)) {
return __('Starting...');
- } else if (this.status === STOPPING) {
+ }
+ if (this.status === STOPPING) {
return __('Stopping...');
}
diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
index 38e53b64503..332408b9ecf 100644
--- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
+++ b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue
@@ -31,12 +31,14 @@ export default {
icon: '',
text: this.isStarted ? MSG_TERMINAL_SYNC_UPLOADING : MSG_TERMINAL_SYNC_CONNECTING,
};
- } else if (this.isError) {
+ }
+ if (this.isError) {
return {
icon: 'warning',
text: this.message,
};
- } else if (this.isStarted) {
+ }
+ if (this.isStarted) {
return {
icon: 'mobile-issue-close',
text: MSG_TERMINAL_SYNC_RUNNING,
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 11a0095db92..2e113003f8a 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -62,9 +62,8 @@ export const initGitlabWebIDE = async (el) => {
filePath,
mrId,
mrTargetProject: getMRTargetProject(),
- // note: At the time of writing this, forkInfo isn't expected by `@gitlab/web-ide`,
- // but it will be soon.
forkInfo,
+ username: gon.current_username,
links: {
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
userPreferences: el.dataset.userPreferencesPath,
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 7595a1cedf1..ec28845d805 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -7,9 +7,11 @@ import DirtyDiffWorker from './diff_worker?worker';
export const getDiffChangeType = (change) => {
if (change.modified) {
return 'modified';
- } else if (change.added) {
+ }
+ if (change.added) {
return 'added';
- } else if (change.removed) {
+ }
+ if (change.removed) {
return 'removed';
}
diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js
index a8a048e588f..5063cf5fd4f 100644
--- a/app/assets/javascripts/ide/lib/errors.js
+++ b/app/assets/javascripts/ide/lib/errors.js
@@ -55,9 +55,11 @@ export const parseCommitError = (e) => {
if (CODEOWNERS_REGEX.test(message)) {
return createCodeownersCommitError(message);
- } else if (BRANCH_CHANGED_REGEX.test(message)) {
+ }
+ if (BRANCH_CHANGED_REGEX.test(message)) {
return createBranchChangedCommitError(message);
- } else if (BRANCH_ALREADY_EXISTS.test(message)) {
+ }
+ if (BRANCH_ALREADY_EXISTS.test(message)) {
return branchAlreadyExistsCommitError(message);
}
diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js
index 3fdf012bbb2..415e34f56b8 100644
--- a/app/assets/javascripts/ide/lib/files.js
+++ b/app/assets/javascripts/ide/lib/files.js
@@ -35,7 +35,8 @@ export const decorateFiles = ({
const insertParent = (path) => {
if (!path) {
return null;
- } else if (entries[path]) {
+ }
+ if (entries[path]) {
return entries[path];
}
diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js
index f437965b25a..286798d7560 100644
--- a/app/assets/javascripts/ide/lib/mirror.js
+++ b/app/assets/javascripts/ide/lib/mirror.js
@@ -32,7 +32,8 @@ const isErrorPayload = (payload) => payload && payload.status_code !== HTTP_STAT
const getErrorFromResponse = (data) => {
if (isErrorResponse(data.error)) {
return { message: data.error.Message };
- } else if (isErrorPayload(data.payload)) {
+ }
+ if (isErrorPayload(data.payload)) {
return { message: data.payload.error_message };
}
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index f4fa52b2d4d..11e3d8260f7 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -133,7 +133,8 @@ export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId }
if (currentProject?.branches?.[branchId]) {
return Promise.resolve();
- } else if (getters.emptyRepo) {
+ }
+ if (getters.emptyRepo) {
return dispatch('loadEmptyBranch', { projectId, branchId });
}
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index c0f666c6652..74fe61b6e2f 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -30,7 +30,8 @@ const getCannotPushCodeViewModel = (state) => {
text: MSG_GO_TO_FORK,
},
};
- } else if (forkPath) {
+ }
+ if (forkPath) {
return {
message: MSG_CANNOT_PUSH_CODE_SHOULD_FORK,
action: {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index ad7ad35a98c..a2b45f9dc62 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -39,7 +39,8 @@ export const configCheckError = (status, helpUrl) => {
},
false,
);
- } else if (status === HTTP_STATUS_FORBIDDEN) {
+ }
+ if (status === HTTP_STATUS_FORBIDDEN) {
return ERROR_PERMISSION;
}
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index ec661fdb0d6..bac3803e68f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -81,9 +81,11 @@ export const setPageTitleForFile = (state, file) => {
export const commitActionForFile = (file) => {
if (file.prevPath) {
return commitActionTypes.move;
- } else if (file.deleted) {
+ }
+ if (file.deleted) {
return commitActionTypes.delete;
- } else if (file.tempFile) {
+ }
+ if (file.tempFile) {
return commitActionTypes.create;
}
@@ -131,7 +133,8 @@ export const createNewMergeRequestUrl = (projectUrl, source, target) =>
const sortTreesByTypeAndName = (a, b) => {
if (a.type === 'tree' && b.type === 'blob') {
return -1;
- } else if (a.type === 'blob' && b.type === 'tree') {
+ }
+ if (a.type === 'blob' && b.type === 'tree') {
return 1;
}
if (a.name < b.name) return -1;
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 94c04123112..91436457b03 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -133,9 +133,11 @@ export default {
if (fetched === imported) {
return { name: 'status-success', class: 'gl-text-green-400' };
- } else if (imported === 0) {
+ }
+ if (imported === 0) {
return { name: 'status-scheduled', class: 'gl-text-gray-400' };
- } else if (this.status === STATUSES.FINISHED) {
+ }
+ if (this.status === STATUSES.FINISHED) {
return { name: 'status-alert', class: 'gl-text-orange-400' };
}
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index e5a88cf9510..782f417a989 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -11,9 +11,9 @@ import {
GlIcon,
GlEmptyState,
} from '@gitlab/ui';
-import { isValidSlaDueAt } from 'ee_else_ce/vue_shared/components/incidents/utils';
import { STATUS_CLOSED } from '~/issues/constants';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
+import { isValidDateString } from '~/lib/utils/datetime_range';
import { s__, n__ } from '~/locale';
import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
@@ -330,7 +330,7 @@ export default {
item.assignees.nodes.length - MAX_VISIBLE_ASSIGNEES,
);
},
- isValidSlaDueAt,
+ isValidDateString,
},
};
</script>
@@ -357,8 +357,7 @@ export default {
<gl-button
v-if="isHeaderButtonVisible"
class="gl-my-3 gl-mr-5 create-incident-button"
- data-testid="createIncidentBtn"
- data-qa-selector="create_incident_button"
+ data-testid="create-incident-button"
:loading="redirecting"
:disabled="redirecting"
category="primary"
@@ -406,7 +405,6 @@ export default {
>
<gl-link
data-testid="incident-link"
- data-qa-selector="incident_link"
:href="showIncidentLink(item)"
class="gl-min-w-0"
>
@@ -443,7 +441,7 @@ export default {
<template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }">
<service-level-agreement-cell
- v-if="isValidSlaDueAt(item.slaDueAt)"
+ v-if="isValidDateString(item.slaDueAt)"
:issue-iid="item.iid"
:project-path="projectPath"
:sla-due-at="item.slaDueAt"
diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
index fe3f4ed4bf9..0a5bb82f151 100644
--- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
+++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
@@ -25,7 +25,7 @@ export default {
<template>
<section
id="incident-management-settings"
- data-qa-selector="incidents_settings_content"
+ data-testid="incidents-settings-content"
class="settings no-animate"
>
<div class="settings-header">
diff --git a/app/assets/javascripts/integrations/index/components/integrations_table.vue b/app/assets/javascripts/integrations/index/components/integrations_table.vue
index eff64ed7c42..c94c509e811 100644
--- a/app/assets/javascripts/integrations/index/components/integrations_table.vue
+++ b/app/assets/javascripts/integrations/index/components/integrations_table.vue
@@ -1,13 +1,22 @@
<script>
-import { GlIcon, GlLink, GlTable, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlButton,
+ GlIcon,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlButton,
GlIcon,
- GlLink,
GlTable,
TimeAgoTooltip,
},
@@ -49,17 +58,12 @@ export default {
key: 'active',
label: '',
thClass: 'gl-w-7',
+ tdClass: 'gl-vertical-align-middle!',
},
{
key: 'title',
label: __('Integration'),
- thClass: 'gl-w-quarter gl-xs-w-full',
- },
- {
- key: 'description',
- label: __('Description'),
- thClass: 'gl-display-none d-sm-table-cell',
- tdClass: 'gl-display-none d-sm-table-cell',
+ thClass: 'd-sm-table-cell',
},
);
@@ -67,11 +71,17 @@ export default {
fields.push({
key: 'updated_at',
label: this.showUpdatedAt ? __('Last updated') : '',
- thClass: 'gl-w-20 gl-text-right',
- tdClass: 'gl-text-right',
+ thClass: 'gl-display-none d-sm-table-cell gl-text-right',
+ tdClass: 'gl-text-right gl-display-none d-sm-table-cell gl-vertical-align-middle!',
});
}
+ fields.push({
+ key: 'edit_path',
+ label: '',
+ thClass: 'gl-w-15',
+ });
+
return fields;
},
filteredIntegrations() {
@@ -83,8 +93,11 @@ export default {
},
methods: {
getStatusTooltipTitle(integration) {
- return sprintf(s__('Integrations|%{integrationTitle}: active'), {
+ const status = integration.active ? 'active' : 'inactive';
+
+ return sprintf(s__('Integrations|%{integrationTitle}: %{status}'), {
integrationTitle: integration.title,
+ status,
});
},
},
@@ -95,26 +108,41 @@ export default {
<gl-table :items="filteredIntegrations" :fields="fields" :empty-text="emptyText" show-empty fixed>
<template #cell(active)="{ item }">
<gl-icon
- v-if="item.active"
+ v-if="item.configured"
v-gl-tooltip
- name="check"
- class="gl-text-green-500"
+ :name="item.active ? 'status-success' : 'status-paused'"
+ :class="item.active ? 'gl-text-green-500' : 'gl-text-gray-500'"
:title="getStatusTooltipTitle(item)"
/>
</template>
<template #cell(title)="{ item }">
- <gl-link
+ <gl-avatar-link
:href="item.edit_path"
- class="gl-font-weight-bold"
+ :title="item.title"
:data-qa-selector="`${item.name}_link`"
>
- {{ item.title }}
- </gl-link>
+ <gl-avatar-labeled
+ :label="item.title"
+ :sub-label="item.description"
+ :entity-id="item.id"
+ :entity-name="item.title"
+ :src="item.icon"
+ :size="32"
+ shape="rect"
+ :label-link="item.edit_path"
+ />
+ </gl-avatar-link>
</template>
<template #cell(updated_at)="{ item }">
<time-ago-tooltip v-if="showUpdatedAt && item.updated_at" :time="item.updated_at" />
</template>
+
+ <template #cell(edit_path)="{ item }">
+ <gl-button :href="item.edit_path">
+ {{ __('Configure') }}
+ </gl-button>
+ </template>
</gl-table>
</template>
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 e99a61caf3f..e9d7acdc913 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,28 +1,14 @@
<script>
-import {
- GlAlert,
- GlCollapsibleListbox,
- GlLink,
- GlSprintf,
- GlFormCheckboxGroup,
- GlButton,
- GlCollapse,
- GlIcon,
-} from '@gitlab/ui';
+import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
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,
- qualifiesForTasksToBeDone,
-} from 'ee_else_ce/invite_members/utils/member_utils';
+import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
import {
USERS_FILTER_ALL,
- INVITE_MEMBERS_FOR_TASK,
MEMBER_MODAL_LABELS,
INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
} from '../constants';
@@ -41,10 +27,6 @@ export default {
name: 'InviteMembersModal',
components: {
GlAlert,
- GlLink,
- GlCollapsibleListbox,
- GlSprintf,
- GlFormCheckboxGroup,
GlButton,
GlCollapse,
GlIcon,
@@ -56,7 +38,6 @@ export default {
import('ee_component/invite_members/components/active_trial_notification.vue'),
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
- inject: ['newProjectPath'],
props: {
id: {
type: String,
@@ -96,14 +77,6 @@ export default {
required: false,
default: null,
},
- tasksToBeDoneOptions: {
- type: Array,
- required: true,
- },
- projects: {
- type: Array,
- required: true,
- },
fullPath: {
type: String,
required: true,
@@ -131,9 +104,6 @@ export default {
modalId: uniqueId('invite-members-modal-'),
newUsersToInvite: [],
invalidMembers: {},
- selectedTasksToBeDone: [],
- selectedTaskProject: this.projects[0],
- selectedTaskProjectId: this.projects[0]?.id,
source: 'unknown',
mode: 'default',
// Kept in sync with "base"
@@ -141,7 +111,6 @@ export default {
errorsLimit: 2,
isErrorsSectionExpanded: false,
shouldShowEmptyInvitesAlert: false,
- projectsForDropdown: this.projects.map((p) => ({ value: p.id, text: p.title, ...p })),
};
},
computed: {
@@ -170,26 +139,6 @@ export default {
this.errorList.length,
);
},
- tasksToBeDoneEnabled() {
- return qualifiesForTasksToBeDone(this.source) && this.tasksToBeDoneOptions.length;
- },
- showTasksToBeDone() {
- return (
- this.tasksToBeDoneEnabled &&
- this.selectedAccessLevel >= INVITE_MEMBERS_FOR_TASK.minimum_access_level
- );
- },
- showTaskProjects() {
- return !this.isProject && this.selectedTasksToBeDone.length;
- },
- tasksToBeDoneForPost() {
- return this.showTasksToBeDone ? this.selectedTasksToBeDone : [];
- },
- tasksProjectForPost() {
- return this.showTasksToBeDone && this.selectedTasksToBeDone.length
- ? this.selectedTaskProject.id
- : '';
- },
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
@@ -243,10 +192,6 @@ export default {
eventHub.$on('openModal', (options) => {
this.openModal(options);
});
-
- if (this.tasksToBeDoneEnabled) {
- this.openModal({ source: 'in_product_marketing_email' });
- }
},
methods: {
showInvalidFeedbackMessage(response) {
@@ -289,8 +234,6 @@ export default {
expires_at: expiresAt,
access_level: accessLevel,
invite_source: this.source,
- tasks_to_be_done: this.tasksToBeDoneForPost,
- tasks_project_id: this.tasksProjectForPost,
...email,
...userId,
};
@@ -304,8 +247,6 @@ export default {
return;
}
- this.trackInviteMembersForTask();
-
const apiAddByInvite = this.isProject
? Api.inviteProjectMembers.bind(Api)
: Api.inviteGroupMembers.bind(Api);
@@ -317,7 +258,7 @@ export default {
const { error, message } = responseFromSuccess(response);
if (error) {
- this.showMemberErrors(message);
+ this.showErrors(message);
} else {
this.onInviteSuccess();
}
@@ -327,19 +268,18 @@ export default {
this.isLoading = false;
}
},
- showMemberErrors(message) {
- this.invalidMembers = message;
- this.$refs.alerts.focus();
+ showErrors(message) {
+ if (isString(message)) {
+ this.invalidFeedbackMessage = message;
+ } else {
+ this.invalidMembers = message;
+ this.$refs.alerts.focus();
+ }
},
tokenName(username) {
// initial token creation hits this and nothing is found... so safe navigation
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
},
- trackInviteMembersForTask() {
- const label = 'selected_tasks_to_be_done';
- const property = this.selectedTasksToBeDone.join(',');
- this.track(INVITE_MEMBERS_FOR_TASK.submit, { label, property });
- },
onCancel() {
this.track('click_cancel', { label: this.source });
},
@@ -351,11 +291,6 @@ export default {
this.isLoading = false;
this.shouldShowEmptyInvitesAlert = false;
this.newUsersToInvite = [];
- this.selectedTasksToBeDone = [];
- [this.selectedTaskProject] = this.projects;
- },
- changeSelectedTaskProject(projectId) {
- this.selectedTaskProject = this.projects.find((project) => project.id === projectId);
},
onInviteSuccess() {
this.track('invite_successful', { label: this.source });
@@ -513,46 +448,5 @@ export default {
@token-remove="removeToken"
/>
</template>
- <template #form-after>
- <div v-if="showTasksToBeDone" data-testid="invite-members-modal-tasks-to-be-done">
- <label class="gl-mt-5">
- {{ $options.labels.tasksToBeDone.title }}
- </label>
- <template v-if="projects.length">
- <gl-form-checkbox-group
- v-model="selectedTasksToBeDone"
- :options="tasksToBeDoneOptions"
- data-testid="invite-members-modal-tasks"
- />
- <template v-if="showTaskProjects">
- <label class="gl-mt-5 gl-display-block">
- {{ $options.labels.tasksProject.title }}
- </label>
- <gl-collapsible-listbox
- v-model="selectedTaskProjectId"
- :items="projectsForDropdown"
- :block="true"
- class="gl-w-half gl-xs-w-full"
- data-testid="invite-members-modal-project-select"
- @select="changeSelectedTaskProject"
- />
- </template>
- </template>
- <gl-alert
- v-else-if="tasksToBeDoneEnabled"
- variant="tip"
- :dismissible="false"
- data-testid="invite-members-modal-no-projects-alert"
- >
- <gl-sprintf :message="$options.labels.tasksToBeDone.noProjects">
- <template #link="{ content }">
- <gl-link :href="newProjectPath" target="_blank" class="gl-label-link">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- </div>
- </template>
</invite-modal-base>
</template>
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 91b623821dd..5a891e23faf 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -330,8 +330,6 @@ export default {
:target="null"
/>
</gl-form-group>
-
- <slot name="form-after"></slot>
</template>
<template v-for="{ key } in extraSlots" #[key]>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 1cee0c32008..93386e5504b 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -5,10 +5,6 @@ export const PROJECT_SELECT_LABEL_ID = 'project-select';
export const SEARCH_DELAY = 200;
export const VALID_TOKEN_BACKGROUND = 'gl-bg-green-100';
export const INVALID_TOKEN_BACKGROUND = 'gl-bg-red-100';
-export const INVITE_MEMBERS_FOR_TASK = {
- minimum_access_level: 30,
- submit: 'submit',
-};
export const TOAST_MESSAGE_LOCALSTORAGE_KEY = 'members_invited_successfully';
export const GROUP_FILTERS = {
@@ -46,15 +42,6 @@ export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
);
export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username or email address');
export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
-export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
- 'InviteMembersModal|Create issues for your new team member to work on (optional)',
-);
-export const MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS = s__(
- 'InviteMembersModal|To assign issues to a new team member, you need a project for the issues. %{linkStart}Create a project to get started.%{linkEnd}',
-);
-export const MEMBERS_TASKS_PROJECTS_TITLE = s__(
- 'InviteMembersModal|Choose a project for the issues',
-);
export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
export const GROUP_MODAL_TO_GROUP_DEFAULT_INTRO_TEXT = s__(
@@ -123,13 +110,6 @@ export const MEMBER_MODAL_LABELS = {
},
searchField: MEMBERS_SEARCH_FIELD,
placeHolder: MEMBERS_PLACEHOLDER,
- tasksToBeDone: {
- title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
- noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
- },
- tasksProject: {
- title: MEMBERS_TASKS_PROJECTS_TITLE,
- },
toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL,
memberErrorListText: MEMBER_ERROR_LIST_TEXT,
collapsedErrors: COLLAPSED_ERRORS,
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 4f539cd8756..41ed0179364 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -25,7 +25,6 @@ export default (function initInviteMembersModal() {
name: 'InviteMembersModalRoot',
provide: {
name: el.dataset.name,
- newProjectPath: el.dataset.newProjectPath,
},
render: (createElement) =>
createElement(InviteMembersModal, {
@@ -34,8 +33,6 @@ export default (function initInviteMembersModal() {
isProject: parseBoolean(el.dataset.isProject),
accessLevels: JSON.parse(el.dataset.accessLevels),
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
- tasksToBeDoneOptions: JSON.parse(el.dataset.tasksToBeDoneOptions || '[]'),
- projects: JSON.parse(el.dataset.projects || '[]'),
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
usersLimitDataset: convertObjectPropsToCamelCase(
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
index 240a3a89686..7998cb69445 100644
--- a/app/assets/javascripts/invite_members/utils/member_utils.js
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -1,5 +1,3 @@
-import { getParameterValues } from '~/lib/utils/url_utility';
-
export function memberName(member) {
// user defined tokens(invites by email) will have email in `name` and will not contain `username`
return member.username || member.name;
@@ -8,7 +6,3 @@ export function memberName(member) {
export function triggerExternalAlert() {
return false;
}
-
-export function qualifiesForTasksToBeDone() {
- return getParameterValues('open_modal')[0] === 'invite_members_for_task';
-}
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue
index c1de507cd80..fd279a6a451 100644
--- a/app/assets/javascripts/issuable/components/csv_export_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue
@@ -47,7 +47,7 @@ export default {
href: this.exportCsvPath,
variant: 'confirm',
'data-method': 'post',
- 'data-qa-selector': `export_issues_button`,
+ 'data-testid': 'export-issues-button',
'data-track-action': 'click_button',
'data-track-label': this.dataTrackLabel,
},
@@ -78,7 +78,7 @@ export default {
:action-cancel="$options.actionCancel"
body-class="gl-p-0!"
:title="exportText"
- data-qa-selector="export_issuable_modal"
+ data-testid="export-issuable-modal"
>
<div
class="gl-justify-content-start gl-align-items-center gl-p-4 gl-border-b-solid gl-border-1 gl-border-gray-50"
diff --git a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
index 872e1d4269d..8e2ed63613d 100644
--- a/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
+++ b/app/assets/javascripts/issuable/components/csv_import_export_buttons.vue
@@ -76,7 +76,6 @@ export default {
v-if="showExportButton"
v-gl-modal="exportModalId"
data-testid="export-as-csv-button"
- data-qa-selector="export_as_csv_button"
:item="dropdownItems.exportAsCSV"
/>
<gl-disclosure-dropdown-item
@@ -88,7 +87,6 @@ export default {
<gl-disclosure-dropdown-item
v-if="showImportButton && canEdit"
data-testid="import-from-jira-link"
- data-qa-selector="import_from_jira_link"
:item="dropdownItems.importFromJIRA"
/>
diff --git a/app/assets/javascripts/issuable/components/issue_assignees.vue b/app/assets/javascripts/issuable/components/issue_assignees.vue
index d8cbc45684b..2181b4e1a40 100644
--- a/app/assets/javascripts/issuable/components/issue_assignees.vue
+++ b/app/assets/javascripts/issuable/components/issue_assignees.vue
@@ -89,7 +89,7 @@ export default {
:img-size="iconSize"
class="js-no-trigger author-link"
tooltip-placement="bottom"
- data-qa-selector="assignee_link"
+ data-testid="assignee-link"
>
<span class="js-assignee-tooltip">
<span class="bold d-block">{{ s__('Label|Assignee') }}</span> {{ assignee.name }}
@@ -101,7 +101,7 @@ export default {
v-gl-tooltip.bottom
:title="assigneesCounterTooltip"
class="avatar-counter"
- data-qa-selector="avatar_counter_content"
+ data-testid="avatar-counter-content"
>{{ assigneeCounterLabel }}</span
>
</div>
diff --git a/app/assets/javascripts/issuable/components/issue_milestone.vue b/app/assets/javascripts/issuable/components/issue_milestone.vue
index c7da3e59098..3340ef2338c 100644
--- a/app/assets/javascripts/issuable/components/issue_milestone.vue
+++ b/app/assets/javascripts/issuable/components/issue_milestone.vue
@@ -42,7 +42,8 @@ export default {
milestoneDatesAbsolute() {
if (this.milestoneDue) {
return `(${dateInWords(this.milestoneDue)})`;
- } else if (this.milestoneStart) {
+ }
+ if (this.milestoneStart) {
return `(${dateInWords(this.milestoneStart)})`;
}
return '';
diff --git a/app/assets/javascripts/issuable/components/status_badge.vue b/app/assets/javascripts/issuable/components/status_badge.vue
new file mode 100644
index 00000000000..949fb3c1ce5
--- /dev/null
+++ b/app/assets/javascripts/issuable/components/status_badge.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlBadge, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import {
+ STATUS_CLOSED,
+ STATUS_LOCKED,
+ STATUS_MERGED,
+ STATUS_OPEN,
+ TYPE_EPIC,
+ TYPE_ISSUE,
+ TYPE_MERGE_REQUEST,
+} from '~/issues/constants';
+
+const badgePropertiesMap = {
+ [TYPE_EPIC]: {
+ [STATUS_OPEN]: {
+ icon: 'epic',
+ text: __('Open'),
+ variant: 'success',
+ },
+ [STATUS_CLOSED]: {
+ icon: 'epic-closed',
+ text: __('Closed'),
+ variant: 'info',
+ },
+ },
+ [TYPE_ISSUE]: {
+ [STATUS_OPEN]: {
+ icon: 'issues',
+ text: __('Open'),
+ variant: 'success',
+ },
+ [STATUS_CLOSED]: {
+ icon: 'issue-closed',
+ text: __('Closed'),
+ variant: 'info',
+ },
+ [STATUS_LOCKED]: {
+ icon: 'issues',
+ text: __('Open'),
+ variant: 'success',
+ },
+ },
+ [TYPE_MERGE_REQUEST]: {
+ [STATUS_OPEN]: {
+ icon: 'merge-request-open',
+ text: __('Open'),
+ variant: 'success',
+ },
+ [STATUS_CLOSED]: {
+ icon: 'merge-request-close',
+ text: __('Closed'),
+ variant: 'danger',
+ },
+ [STATUS_MERGED]: {
+ icon: 'merge',
+ text: __('Merged'),
+ variant: 'info',
+ },
+ [STATUS_LOCKED]: {
+ icon: 'merge-request-open',
+ text: __('Open'),
+ variant: 'success',
+ },
+ },
+};
+
+export default {
+ components: {
+ GlBadge,
+ GlIcon,
+ },
+ props: {
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ state: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ badgeProperties() {
+ return badgePropertiesMap[this.issuableType][this.state];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge :variant="badgeProperties.variant" :aria-label="badgeProperties.text">
+ <gl-icon :name="badgeProperties.icon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeProperties.text }}</span>
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/issuable/components/status_box.vue b/app/assets/javascripts/issuable/components/status_box.vue
deleted file mode 100644
index 0d7d0f020dd..00000000000
--- a/app/assets/javascripts/issuable/components/status_box.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-<script>
-import { GlBadge, GlIcon } from '@gitlab/ui';
-import Vue from 'vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { fetchPolicies } from '~/lib/graphql';
-import { __ } from '~/locale';
-import {
- STATUS_CLOSED,
- STATUS_OPEN,
- TYPE_ISSUE,
- TYPE_MERGE_REQUEST,
- TYPE_EPIC,
-} from '~/issues/constants';
-
-export const badgeState = Vue.observable({
- state: '',
- updateStatus: null,
-});
-
-const CLASSES = {
- opened: 'issuable-status-badge-open',
- locked: 'issuable-status-badge-open',
- closed: 'issuable-status-badge-closed',
- merged: 'issuable-status-badge-merged',
-};
-
-const ICONS = {
- [TYPE_EPIC]: {
- opened: 'epic',
- closed: 'epic-closed',
- },
- [TYPE_ISSUE]: {
- opened: 'issues',
- locked: 'issues',
- closed: 'issue-closed',
- },
- [TYPE_MERGE_REQUEST]: {
- opened: 'merge-request-open',
- locked: 'merge-request-open',
- closed: 'merge-request-close',
- merged: 'merge',
- },
-};
-
-const STATUS = {
- opened: __('Open'),
- locked: __('Open'),
- closed: __('Closed'),
- merged: __('Merged'),
-};
-
-export default {
- components: {
- GlBadge,
- GlIcon,
- },
- mixins: [glFeatureFlagMixin()],
- inject: {
- query: { default: null },
- projectPath: { default: null },
- iid: { default: null },
- },
- props: {
- initialState: {
- type: String,
- required: false,
- default: null,
- },
- issuableType: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- if (!this.iid) return { state: this.initialState };
-
- if (this.initialState && !badgeState.state) {
- badgeState.state = this.initialState;
- }
-
- return badgeState;
- },
- computed: {
- badgeClass() {
- return [
- CLASSES[this.state],
- {
- 'gl-vertical-align-bottom': this.issuableType === TYPE_MERGE_REQUEST,
- },
- ];
- },
- badgeVariant() {
- if (this.state === STATUS_OPEN) {
- return 'success';
- } else if (this.state === STATUS_CLOSED) {
- return this.issuableType === TYPE_MERGE_REQUEST ? 'danger' : 'info';
- }
- return 'info';
- },
- badgeText() {
- return STATUS[this.state];
- },
- badgeIcon() {
- const type = this.issuableType || TYPE_MERGE_REQUEST;
- return ICONS[type][this.state];
- },
- },
- created() {
- if (!badgeState.updateStatus) {
- badgeState.updateStatus = this.fetchState;
- }
- },
- beforeDestroy() {
- if (badgeState.updateStatus && this.query) {
- badgeState.updateStatus = null;
- }
- },
- methods: {
- async fetchState() {
- const { data } = await this.$apollo.query({
- query: this.query,
- variables: {
- projectPath: this.projectPath,
- iid: this.iid,
- },
- fetchPolicy: fetchPolicies.NO_CACHE,
- });
-
- badgeState.state = data?.workspace?.issuable?.state;
- },
- },
-};
-</script>
-
-<template>
- <gl-badge
- class="issuable-status-badge gl-mr-3 gl-align-self-center"
- :class="badgeClass"
- :variant="badgeVariant"
- :aria-label="badgeText"
- >
- <gl-icon :name="badgeIcon" class="gl-badge-icon" />
- <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ badgeText }}</span>
- </gl-badge>
-</template>
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
index acc0161bf6a..40f92763b29 100644
--- a/app/assets/javascripts/issuable/index.js
+++ b/app/assets/javascripts/issuable/index.js
@@ -5,7 +5,6 @@ import Sidebar from '~/right_sidebar';
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
import IssuableByEmail from './components/issuable_by_email.vue';
-import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableContext from './issuable_context';
@@ -97,24 +96,6 @@ export function initIssuableByEmail() {
});
}
-export function initIssuableHeaderWarnings(store) {
- const el = document.getElementById('js-issuable-header-warnings');
-
- if (!el) {
- return null;
- }
-
- const { hidden } = el.dataset;
-
- return new Vue({
- el,
- name: 'IssuableHeaderWarningsRoot',
- store,
- provide: { hidden: parseBoolean(hidden) },
- render: (createElement) => createElement(IssuableHeaderWarnings),
- });
-}
-
export function initIssuableSidebar() {
const el = document.querySelector('.js-sidebar-options');
diff --git a/app/assets/javascripts/issuable/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js
index 8c2e2a5df67..ef49bd29a40 100644
--- a/app/assets/javascripts/issuable/issuable_context.js
+++ b/app/assets/javascripts/issuable/issuable_context.js
@@ -8,6 +8,29 @@ export default class IssuableContext {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
+ this.reviewersSelect.dropdowns.forEach((glDropdownInstance) => {
+ const jQueryWrapper = glDropdownInstance.dropdown;
+ const domElement = jQueryWrapper[0];
+ const content = domElement.querySelector('.dropdown-content');
+ const loader = domElement.querySelector('.dropdown-loading');
+ const spinner = loader.querySelector('.gl-spinner-container');
+ const realParent = loader.parentNode;
+
+ domElement.classList.add('non-blocking-loader');
+ spinner.classList.remove('gl-mt-7');
+ spinner.classList.add('gl-mt-2');
+
+ jQueryWrapper.on('shown.bs.dropdown', () => {
+ glDropdownInstance.filterInput.focus();
+ });
+ jQueryWrapper.on('toggle.on.loading.gl.dropdown filtering.gl.dropdown', () => {
+ content.appendChild(loader);
+ });
+ jQueryWrapper.on('done.remote.loading.gl.dropdown done.filtering.gl.dropdown', () => {
+ realParent.appendChild(loader);
+ });
+ });
+
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
index 044a1bba7ad..12d76ec4b54 100644
--- a/app/assets/javascripts/issuable/popover/components/issue_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
@@ -3,12 +3,13 @@ import { GlIcon, GlPopover, GlSkeletonLoader, GlTooltipDirective } from '@gitlab
import query from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
-import StatusBox from '~/issuable/components/status_box.vue';
-import { STATUS_CLOSED } from '~/issues/constants';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+import { STATUS_CLOSED, TYPE_ISSUE } from '~/issues/constants';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
export default {
+ TYPE_ISSUE,
components: {
GlIcon,
GlPopover,
@@ -16,7 +17,7 @@ export default {
IssueDueDate,
IssueMilestone,
IssueWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
- StatusBox,
+ StatusBadge,
WorkItemTypeIcon,
},
directives: {
@@ -82,14 +83,14 @@ export default {
<gl-skeleton-loader v-if="$apollo.queries.issue.loading" :height="15">
<rect width="250" height="15" rx="4" />
</gl-skeleton-loader>
- <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center">
- <status-box issuable-type="issue" :initial-state="issue.state" />
+ <div v-else-if="showDetails" class="gl-display-flex gl-align-items-center gl-gap-2">
+ <status-badge :issuable-type="$options.TYPE_ISSUE" :state="issue.state" />
<gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
- class="gl-text-orange-500 gl-mr-2"
+ class="gl-text-orange-500"
:aria-label="__('Confidential')"
/>
<span class="gl-text-secondary">
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 0a1a1324d7d..80344efc44c 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -30,6 +30,7 @@ export const issuableStatusText = {
export const IssuableTypeText = {
[TYPE_ISSUE]: __('issue'),
+ [TYPE_EPIC]: __('epic'),
[TYPE_MERGE_REQUEST]: __('merge request'),
[TYPE_ALERT]: __('alert'),
[TYPE_INCIDENT]: __('incident'),
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 9febebf7e55..a756229e6ca 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -495,7 +495,6 @@ export default {
:issuables-loading="isLoading"
namespace="dashboard"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index eec7c6bf842..3bd28c50800 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -3,21 +3,20 @@ import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable';
-import { TYPE_INCIDENT } from '~/issues/constants';
+import { initIssuableSidebar } from '~/issuable';
import Issue from '~/issues/issue';
import { initTitleSuggestions, initTypePopover, initTypeSelect } from '~/issues/new';
import { initRelatedMergeRequests } from '~/issues/related_merge_requests';
import { initRelatedIssues } from '~/related_issues';
-import { initIncidentApp, initIssueApp, initSentryErrorStackTrace } from '~/issues/show';
-import { parseIssuableData } from '~/issues/show/utils/parse_data';
+import { initIssuableApp, initSentryErrorStackTrace } from '~/issues/show';
import LabelsSelect from '~/labels/labels_select';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initWorkItemLinks from '~/work_items/components/work_item_links';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
-import initLinkedResources from '~/linked_resources';
import FilteredSearchServiceDesk from './filtered_search_service_desk';
export function initFilteredSearchServiceDesk() {
@@ -42,33 +41,20 @@ export function initForm() {
mountMilestoneDropdown();
}
-export function initShow({ notesParams } = {}) {
- const el = document.getElementById('js-issuable-app');
-
- if (!el) {
- return;
- }
-
- const { issueType, ...issuableData } = parseIssuableData(el);
-
- if (issueType === TYPE_INCIDENT) {
- initIncidentApp({ ...issuableData, issuableId: el.dataset.issuableId }, store);
- initLinkedResources();
- initRelatedIssues(TYPE_INCIDENT);
- } else {
- initIssueApp(issuableData, store);
- }
-
+export function initShow() {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
- initIssuableHeaderWarnings(store);
+
+ initAwardsApp(document.getElementById('js-vue-awards-block'));
+ initIssuableApp(store);
initIssuableSidebar();
- initNotesApp(notesParams);
+ initNotesApp();
+ initRelatedIssues();
initRelatedMergeRequests();
initSentryErrorStackTrace();
-
- initAwardsApp(document.getElementById('js-vue-awards-block'));
+ initSidebarBundle(store);
+ initWorkItemLinks();
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index b7fd99d8042..06bbcdc12ea 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -49,13 +49,8 @@ export default class Issue {
issueFailMessage = __('Unable to update this issue at this time.'),
) {
if ('id' in data) {
- const isClosedBadge = $('.issuable-status-badge-closed');
- const isOpenBadge = $('.issuable-status-badge-open');
const projectIssuesCounter = $('.issue_counter');
- isClosedBadge.toggleClass('hidden', !isClosed);
- isOpenBadge.toggleClass('hidden', isClosed);
-
$(document).trigger('issuable:change', isClosed);
let numProjectIssues = Number(
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
index 9f7fca0ceca..3d62ea07f59 100644
--- a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
@@ -82,7 +82,7 @@ export default {
v-if="showCsvButtons"
class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
:toggle-text="$options.i18n.importIssues"
- data-qa-selector="import_issues_dropdown"
+ data-testid="import-issues-dropdown"
>
<csv-import-export-buttons
:export-csv-path="exportCsvPathWithQuery"
diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
index dde1a4fd2d6..22c0984ebdb 100644
--- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
@@ -10,6 +10,8 @@ import {
newDateAsLocaleTime,
} from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
+import { STATE_CLOSED } from '~/work_items/constants';
+import { isMilestoneWidget, isStartAndDueDateWidget } from '~/work_items/utils';
export default {
components: {
@@ -26,9 +28,12 @@ export default {
},
},
computed: {
+ milestone() {
+ return this.issue.milestone || this.issue.widgets?.find(isMilestoneWidget)?.milestone;
+ },
milestoneDate() {
- if (this.issue.milestone?.dueDate) {
- const { dueDate, startDate } = this.issue.milestone;
+ if (this.milestone.dueDate) {
+ const { dueDate, startDate } = this.milestone;
const date = dateInWords(newDateAsLocaleTime(dueDate), true);
const remainingTime = this.milestoneRemainingTime(dueDate, startDate);
return `${date} (${remainingTime})`;
@@ -36,15 +41,19 @@ export default {
return __('Milestone');
},
milestoneLink() {
- return this.issue.milestone.webPath || this.issue.milestone.webUrl;
+ return this.milestone.webPath || this.milestone.webUrl;
},
dueDate() {
- return this.issue.dueDate && dateInWords(newDateAsLocaleTime(this.issue.dueDate), true);
+ return this.issue.dueDate || this.issue.widgets?.find(isStartAndDueDateWidget)?.dueDate;
+ },
+ dueDateText() {
+ return this.dueDate && dateInWords(newDateAsLocaleTime(this.dueDate), true);
+ },
+ isClosed() {
+ return this.issue.state === STATUS_CLOSED || this.issue.state === STATE_CLOSED;
},
showDueDateInRed() {
- return (
- isInPast(newDateAsLocaleTime(this.issue.dueDate)) && this.issue.state !== STATUS_CLOSED
- );
+ return isInPast(newDateAsLocaleTime(this.dueDate)) && !this.isClosed;
},
timeEstimate() {
return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate;
@@ -57,11 +66,14 @@ export default {
if (dueDate && isInPast(due)) {
return __('Past due');
- } else if (dueDate && isToday(due)) {
+ }
+ if (dueDate && isToday(due)) {
return __('Today');
- } else if (startDate && isInFuture(start)) {
+ }
+ if (startDate && isInFuture(start)) {
return __('Upcoming');
- } else if (dueDate) {
+ }
+ if (dueDate) {
return getTimeRemainingInWords(due);
}
return '';
@@ -73,7 +85,7 @@ export default {
<template>
<span>
<span
- v-if="issue.milestone"
+ v-if="milestone"
class="issuable-milestone gl-mr-3 gl-text-truncate gl-max-w-26 gl-display-inline-block gl-vertical-align-bottom"
data-testid="issuable-milestone"
>
@@ -84,11 +96,11 @@ export default {
class="gl-font-sm gl-text-gray-500!"
>
<gl-icon name="clock" :size="12" />
- {{ issue.milestone.title }}
+ {{ milestone.title }}
</gl-link>
</span>
<span
- v-if="issue.dueDate"
+ v-if="dueDate"
v-gl-tooltip
class="issuable-due-date gl-mr-3"
:class="{ 'gl-text-red-500': showDueDateInRed }"
@@ -96,7 +108,7 @@ export default {
data-testid="issuable-due-date"
>
<gl-icon name="calendar" :size="12" />
- {{ dueDate }}
+ {{ dueDateText }}
</span>
<span
v-if="timeEstimate"
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 c50b48ca0d8..3d8ed3af816 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -99,8 +99,6 @@ import {
import eventHub from '../eventhub';
import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
-import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
-import searchUsersQuery from '../queries/search_users.query.graphql';
import setSortPreferenceMutation from '../queries/set_sort_preference.mutation.graphql';
import {
convertToApiParams,
@@ -204,11 +202,6 @@ export default {
required: false,
default: () => [],
},
- eeIsOkrsEnabled: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -411,9 +404,10 @@ export default {
title: TOKEN_TITLE_MILESTONE,
icon: 'clock',
token: MilestoneToken,
- fetchMilestones: this.fetchMilestones,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
shouldSkipSort: true,
+ fullPath: this.fullPath,
+ isProject: this.isProject,
},
{
type: TOKEN_TYPE_LABEL,
@@ -640,32 +634,13 @@ export default {
fetchLatestLabels(search) {
return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
},
- fetchMilestones(search) {
- return this.$apollo
- .query({
- query: searchMilestonesQuery,
- variables: { fullPath: this.fullPath, search, isProject: this.isProject },
- })
- .then(({ data }) => data[this.namespace]?.milestones.nodes);
- },
fetchUsers(search) {
- if (gon.features?.newGraphqlUsersAutocomplete) {
- return this.$apollo
- .query({
- query: usersAutocompleteQuery,
- variables: { fullPath: this.fullPath, search, isProject: this.isProject },
- })
- .then(({ data }) => data[this.namespace]?.autocompleteUsers);
- }
-
return this.$apollo
.query({
- query: searchUsersQuery,
+ query: usersAutocompleteQuery,
variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
- .then(({ data }) =>
- data[this.namespace]?.[`${this.namespace}Members`].nodes.map((member) => member.user),
- );
+ .then(({ data }) => data[this.namespace]?.autocompleteUsers);
},
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
@@ -966,7 +941,6 @@ export default {
v-if="hasAnyIssues"
:namespace="fullPath"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:has-scoped-labels-feature="hasScopedLabelsFeature"
:initial-filter-value="filterTokens"
@@ -1037,14 +1011,11 @@ export default {
>
{{ $options.i18n.editIssues }}
</gl-button>
- <gl-button
- v-if="showNewIssueLink && !eeIsOkrsEnabled"
- :href="newIssuePath"
- variant="confirm"
- >
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- <slot name="new-objective-button"></slot>
+ <slot name="new-issuable-button">
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </slot>
<new-resource-dropdown
v-if="showNewIssueDropdown"
:query="$options.searchProjectsQuery"
@@ -1059,7 +1030,7 @@ export default {
no-caret
:toggle-text="$options.i18n.actionsLabel"
text-sr-only
- data-qa-selector="issues_list_more_actions_dropdown"
+ data-testid="issues-list-more-actions-dropdown"
>
<csv-import-export-buttons
v-if="showCsvButtons"
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 85e300b6474..682c7629962 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -121,7 +121,6 @@ export const i18n = {
reorderError: __('An error occurred while reordering issues.'),
deleteError: __('An error occurred while deleting an issuable.'),
rssLabel: __('Subscribe to RSS feed'),
- searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
titles: __('Titles'),
descriptions: __('Descriptions'),
diff --git a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index 3b49c0efb14..f3173f0e33a 100644
--- a/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
@@ -11,6 +11,7 @@ fragment IssueFragment on Issue {
moved
state
title
+ titleHtml
updatedAt
closedAt
upvotes
diff --git a/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
index 040240cde99..941e71b7ca7 100644
--- a/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
@@ -1,6 +1,11 @@
#import "./milestone.fragment.graphql"
-query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = false) {
+query searchMilestones(
+ $fullPath: ID!
+ $search: String
+ $isProject: Boolean = false
+ $state: MilestoneStateEnum
+) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
milestones(
@@ -8,7 +13,7 @@ query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = fa
includeAncestors: true
includeDescendants: true
sort: EXPIRED_LAST_DUE_DATE_ASC
- state: active
+ state: $state
) {
nodes {
...Milestone
@@ -21,7 +26,7 @@ query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = fa
searchTitle: $search
includeAncestors: true
sort: EXPIRED_LAST_DUE_DATE_ASC
- state: active
+ state: $state
) {
nodes {
...Milestone
diff --git a/app/assets/javascripts/issues/list/queries/search_users.query.graphql b/app/assets/javascripts/issues/list/queries/search_users.query.graphql
deleted file mode 100644
index 6a1967a8875..00000000000
--- a/app/assets/javascripts/issues/list/queries/search_users.query.graphql
+++ /dev/null
@@ -1,29 +0,0 @@
-#import "./user.fragment.graphql"
-
-query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false) {
- group(fullPath: $fullPath) @skip(if: $isProject) {
- id
- groupMembers(search: $search, relations: [DIRECT, INHERITED, SHARED_FROM_GROUPS]) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
- project(fullPath: $fullPath) @include(if: $isProject) {
- id
- projectMembers(
- search: $search
- relations: [DIRECT, INHERITED, INVITED_GROUPS, SHARED_INTO_ANCESTORS]
- ) {
- nodes {
- id
- user {
- ...User
- }
- }
- }
- }
-}
diff --git a/app/assets/javascripts/issues/list/queries/user.fragment.graphql b/app/assets/javascripts/issues/list/queries/user.fragment.graphql
deleted file mode 100644
index 3e5bc0f7b93..00000000000
--- a/app/assets/javascripts/issues/list/queries/user.fragment.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-fragment User on User {
- id
- avatarUrl
- name
- username
-}
diff --git a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
index d819a371c69..5e81f7ad4f6 100644
--- a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue
@@ -4,7 +4,6 @@ import { GlLink, GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { sprintf, __, n__ } from '~/locale';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
-import { parseIssuableData } from '~/issues/show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
@@ -19,6 +18,11 @@ export default {
type: String,
required: true,
},
+ hasClosingMergeRequest: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
projectNamespace: {
type: String,
required: true,
@@ -48,9 +52,6 @@ export default {
this.setInitialState({ apiEndpoint: this.endpoint });
this.fetchMergeRequests();
},
- created() {
- this.hasClosingMergeRequest = parseIssuableData().hasClosingMergeRequest;
- },
methods: {
...mapActions(['setInitialState', 'fetchMergeRequests']),
getAssignees(mr) {
diff --git a/app/assets/javascripts/issues/related_merge_requests/index.js b/app/assets/javascripts/issues/related_merge_requests/index.js
index 196084093c8..413b48b9720 100644
--- a/app/assets/javascripts/issues/related_merge_requests/index.js
+++ b/app/assets/javascripts/issues/related_merge_requests/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import RelatedMergeRequests from './components/related_merge_requests.vue';
import createStore from './store';
@@ -9,7 +10,7 @@ export function initRelatedMergeRequests() {
return undefined;
}
- const { endpoint, projectPath, projectNamespace } = el.dataset;
+ const { endpoint, hasClosingMergeRequest, projectPath, projectNamespace } = el.dataset;
return new Vue({
el,
@@ -17,7 +18,12 @@ export function initRelatedMergeRequests() {
store: createStore(),
render: (createElement) =>
createElement(RelatedMergeRequests, {
- props: { endpoint, projectNamespace, projectPath },
+ props: {
+ endpoint,
+ hasClosingMergeRequest: parseBoolean(hasClosingMergeRequest),
+ projectNamespace,
+ projectPath,
+ },
}),
});
}
diff --git a/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
index a15c8ee2e9f..ab9e70ae223 100644
--- a/app/assets/javascripts/service_desk/components/empty_state_with_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_with_any_issues.vue
@@ -38,7 +38,8 @@ export default {
description: noSearchResultsDescription,
svgHeight: 150,
};
- } else if (this.isOpenTab) {
+ }
+ if (this.isOpenTab) {
return { title: noOpenIssuesTitle, description: infoBannerUserNote };
}
diff --git a/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
index 9dbed2c2579..9dbed2c2579 100644
--- a/app/assets/javascripts/service_desk/components/empty_state_without_any_issues.vue
+++ b/app/assets/javascripts/issues/service_desk/components/empty_state_without_any_issues.vue
diff --git a/app/assets/javascripts/service_desk/components/info_banner.vue b/app/assets/javascripts/issues/service_desk/components/info_banner.vue
index 5667ee2f31d..5667ee2f31d 100644
--- a/app/assets/javascripts/service_desk/components/info_banner.vue
+++ b/app/assets/javascripts/issues/service_desk/components/info_banner.vue
diff --git a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
index 56cd21d7ea9..4b59672428b 100644
--- a/app/assets/javascripts/service_desk/components/service_desk_list_app.vue
+++ b/app/assets/javascripts/issues/service_desk/components/service_desk_list_app.vue
@@ -5,7 +5,8 @@ import { isEmpty } from 'lodash';
import { fetchPolicies } from '~/lib/graphql';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import axios from '~/lib/utils/axios_utils';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
+import { scrollUp } from '~/lib/utils/scroll_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
@@ -15,6 +16,8 @@ import {
getInitialPageParams,
getFilterTokens,
isSortKey,
+ getSortOptions,
+ getSortKey,
} from '~/issues/list/utils';
import {
OPERATORS_IS_NOT,
@@ -31,19 +34,24 @@ import {
PARAM_SORT,
CREATED_DESC,
UPDATED_DESC,
+ RELATIVE_POSITION_ASC,
urlSortParams,
} from '~/issues/list/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { createAlert, VARIANT_INFO } from '~/alert';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import searchProjectMembers from '~/graphql_shared/queries/project_user_members_search.query.graphql';
-import getServiceDeskIssuesQuery from 'ee_else_ce/service_desk/queries/get_service_desk_issues.query.graphql';
-import getServiceDeskIssuesCounts from 'ee_else_ce/service_desk/queries/get_service_desk_issues_counts.query.graphql';
+import getServiceDeskIssuesQuery from 'ee_else_ce/issues/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCounts from 'ee_else_ce/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql';
import searchProjectLabelsQuery from '../queries/search_project_labels.query.graphql';
import searchProjectMilestonesQuery from '../queries/search_project_milestones.query.graphql';
+import setSortingPreferenceMutation from '../queries/set_sorting_preference.mutation.graphql';
+import reorderServiceDeskIssuesMutation from '../queries/reorder_service_desk_issues.mutation.graphql';
import {
errorFetchingCounts,
errorFetchingIssues,
- searchPlaceholder,
+ issueRepositioningMessage,
+ reorderError,
SERVICE_DESK_BOT_USERNAME,
STATUS_OPEN,
STATUS_CLOSED,
@@ -68,7 +76,8 @@ export default {
i18n: {
errorFetchingCounts,
errorFetchingIssues,
- searchPlaceholder,
+ issueRepositioningMessage,
+ reorderError,
},
issuableListTabs,
components: {
@@ -81,6 +90,7 @@ export default {
inject: [
'releasesPath',
'autocompleteAwardEmojisPath',
+ 'hasBlockedIssuesFeature',
'hasIterationsFeature',
'hasIssueWeightsFeature',
'hasIssuableHealthStatusFeature',
@@ -92,6 +102,7 @@ export default {
'isServiceDeskSupported',
'hasAnyIssues',
'initialSort',
+ 'isIssueRepositioningDisabled',
],
props: {
eeSearchTokens: {
@@ -104,14 +115,13 @@ export default {
return {
serviceDeskIssues: [],
serviceDeskIssuesCounts: {},
- sortOptions: [],
filterTokens: [],
pageInfo: {},
pageParams: {},
sortKey: CREATED_DESC,
state: STATUS_OPEN,
pageSize: DEFAULT_PAGE_SIZE,
- issuesError: null,
+ issuesError: '',
};
},
apollo: {
@@ -167,7 +177,6 @@ export default {
return {
fullPath: this.fullPath,
iid: isIidSearch ? this.searchQuery.slice(1) : undefined,
- isProject: this.isProject,
isSignedIn: this.isSignedIn,
authorUsername: SERVICE_DESK_BOT_USERNAME,
sort: this.sortKey,
@@ -180,6 +189,13 @@ export default {
shouldSkipQuery() {
return !this.hasAnyIssues || isEmpty(this.pageParams);
},
+ sortOptions() {
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
+ },
tabCounts() {
const { openedIssues, closedIssues, allIssues } = this.serviceDeskIssuesCounts;
return {
@@ -188,8 +204,20 @@ export default {
[STATUS_ALL]: allIssues?.count,
};
},
+ currentTabCount() {
+ return this.tabCounts[this.state] ?? 0;
+ },
+ showPaginationControls() {
+ return (
+ this.serviceDeskIssues.length > 0 &&
+ (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage)
+ );
+ },
+ showPageSizeControls() {
+ return this.currentTabCount > DEFAULT_PAGE_SIZE;
+ },
isLoading() {
- return this.$apollo.queries.serviceDeskIssues.loading;
+ return this.$apollo.loading;
},
isOpenTab() {
return this.state === STATUS_OPEN;
@@ -205,11 +233,14 @@ export default {
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
+ hasAnyServiceDeskIssue() {
+ return this.hasSearch || Boolean(this.tabCounts.all);
+ },
isInfoBannerVisible() {
- return this.isServiceDeskSupported && this.hasAnyServiceDeskIssues;
+ return this.isServiceDeskSupported && this.hasAnyServiceDeskIssue;
},
- hasAnyServiceDeskIssues() {
- return this.hasSearch || Boolean(this.tabCounts.all);
+ canShowIssuesList() {
+ return this.isLoading || this.issuesError.length || this.hasAnyServiceDeskIssue;
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
@@ -296,6 +327,9 @@ export default {
return tokens;
},
+ isManualOrdering() {
+ return this.sortKey === RELATIVE_POSITION_ASC;
+ },
},
watch: {
$route(newValue, oldValue) {
@@ -383,15 +417,120 @@ export default {
this.$router.push({ query: this.urlParams });
},
+ handleDismissAlert() {
+ this.issuesError = '';
+ },
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: this.pageSize,
+ };
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: this.pageSize,
+ };
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handlePageSizeChange(newPageSize) {
+ const pageParam = getParameterByName(PARAM_LAST_PAGE_SIZE) ? 'lastPageSize' : 'firstPageSize';
+ this.pageParams[pageParam] = newPageSize;
+ this.pageSize = newPageSize;
+ scrollUp();
+
+ this.$router.push({ query: this.urlParams });
+ },
+ handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ return;
+ }
+
+ this.sortKey = sortKey;
+ this.pageParams = getInitialPageParams(this.pageSize);
+
+ if (this.isSignedIn) {
+ this.saveSortPreference(sortKey);
+ }
+
+ this.$router.push({ query: this.urlParams });
+ },
+ saveSortPreference(sortKey) {
+ this.$apollo
+ .mutate({
+ mutation: setSortingPreferenceMutation,
+ variables: { input: { issuesSort: sortKey } },
+ })
+ .then(({ data }) => {
+ if (data.userPreferencesUpdate.errors.length) {
+ throw new Error(data.userPreferencesUpdate.errors);
+ }
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ });
+ },
+ handleReorder({ newIndex, oldIndex }) {
+ const issueToMove = this.serviceDeskIssues[oldIndex];
+ const isDragDropDownwards = newIndex > oldIndex;
+ const isMovingToBeginning = newIndex === 0;
+ const isMovingToEnd = newIndex === this.serviceDeskIssues.length - 1;
+
+ let moveBeforeId;
+ let moveAfterId;
+
+ if (isDragDropDownwards) {
+ const afterIndex = isMovingToEnd ? newIndex : newIndex + 1;
+ moveBeforeId = this.serviceDeskIssues[newIndex].id;
+ moveAfterId = this.serviceDeskIssues[afterIndex].id;
+ } else {
+ const beforeIndex = isMovingToBeginning ? newIndex : newIndex - 1;
+ moveBeforeId = this.serviceDeskIssues[beforeIndex].id;
+ moveAfterId = this.serviceDeskIssues[newIndex].id;
+ }
+
+ return axios
+ .put(joinPaths(issueToMove.webPath, 'reorder'), {
+ move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
+ move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
+ })
+ .then(() => {
+ const serializedVariables = JSON.stringify(this.queryVariables);
+ return this.$apollo.mutate({
+ mutation: reorderServiceDeskIssuesMutation,
+ variables: { oldIndex, newIndex, namespace: this.namespace, serializedVariables },
+ });
+ })
+ .catch((error) => {
+ this.issuesError = this.$options.i18n.reorderError;
+ Sentry.captureException(error);
+ });
+ },
updateData(sortValue) {
const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === STATUS_CLOSED ? UPDATED_DESC : CREATED_DESC;
+ const dashboardSortKey = getSortKey(sortValue);
const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
- const sortKey = graphQLSortKey || defaultSortKey;
+ let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
+
+ if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
+ this.showIssueRepositioningMessage();
+ sortKey = defaultSortKey;
+ }
this.filterTokens = getFilterTokens(window.location.search);
@@ -405,6 +544,12 @@ export default {
this.sortKey = sortKey;
this.state = state || STATUS_OPEN;
},
+ showIssueRepositioningMessage() {
+ createAlert({
+ message: this.$options.i18n.issueRepositioningMessage,
+ variant: VARIANT_INFO,
+ });
+ },
},
};
</script>
@@ -413,25 +558,36 @@ export default {
<section>
<info-banner v-if="isInfoBannerVisible" />
<issuable-list
- v-if="isLoading || hasAnyServiceDeskIssues"
+ v-if="canShowIssuesList"
namespace="service-desk"
recent-searches-storage-key="service-desk-issues"
:error="issuesError"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
:issuables-loading="isLoading"
:initial-filter-value="filterTokens"
:show-filtered-search-friendly-text="hasOrFeature"
+ :show-pagination-controls="showPaginationControls"
+ :show-page-size-change-controls="showPageSizeControls"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
+ :is-manual-ordering="isManualOrdering"
:issuables="serviceDeskIssues"
:tabs="$options.issuableListTabs"
:tab-counts="tabCounts"
:current-tab="state"
:default-page-size="pageSize"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
sync-filter-and-sort
+ use-keyset-pagination
@click-tab="handleClickTab"
+ @dismiss-alert="handleDismissAlert"
@filter="handleFilter"
+ @sort="handleSort"
+ @reorder="handleReorder"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
+ @page-size-change="handlePageSizeChange"
>
<template #empty-state>
<empty-state-with-any-issues :has-search="hasSearch" :is-open-tab="isOpenTab" />
diff --git a/app/assets/javascripts/service_desk/constants.js b/app/assets/javascripts/issues/service_desk/constants.js
index a83c0d9ca57..e498a4f39a1 100644
--- a/app/assets/javascripts/service_desk/constants.js
+++ b/app/assets/javascripts/issues/service_desk/constants.js
@@ -235,7 +235,10 @@ export const noSearchResultsDescription = __(
'To widen your search, change or remove filters above',
);
export const noSearchResultsTitle = __('Sorry, your filter produced no results');
-export const searchPlaceholder = __('Search or filter results...');
+export const issueRepositioningMessage = __(
+ 'Issues are being rebalanced at the moment, so manual reordering is disabled.',
+);
+export const reorderError = __('An error occurred while reordering issues.');
export const infoBannerTitle = s__(
'ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab',
);
diff --git a/app/assets/javascripts/service_desk/graphql.js b/app/assets/javascripts/issues/service_desk/graphql.js
index e01973f1e8a..e01973f1e8a 100644
--- a/app/assets/javascripts/service_desk/graphql.js
+++ b/app/assets/javascripts/issues/service_desk/graphql.js
diff --git a/app/assets/javascripts/service_desk/index.js b/app/assets/javascripts/issues/service_desk/index.js
index afb2d0e8de3..579cf343477 100644
--- a/app/assets/javascripts/service_desk/index.js
+++ b/app/assets/javascripts/issues/service_desk/index.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import { parseBoolean } from '~/lib/utils/common_utils';
-import ServiceDeskListApp from 'ee_else_ce/service_desk/components/service_desk_list_app.vue';
+import ServiceDeskListApp from 'ee_else_ce/issues/service_desk/components/service_desk_list_app.vue';
import { gqlClient } from './graphql';
export async function mountServiceDeskListApp() {
@@ -15,6 +15,7 @@ export async function mountServiceDeskListApp() {
const {
projectDataReleasesPath,
projectDataAutocompleteAwardEmojisPath,
+ projectDataHasBlockedIssuesFeature,
projectDataHasIterationsFeature,
projectDataHasIssueWeightsFeature,
projectDataHasIssuableHealthStatusFeature,
@@ -26,6 +27,7 @@ export async function mountServiceDeskListApp() {
projectDataSignInPath,
projectDataHasAnyIssues,
projectDataInitialSort,
+ projectDataIsIssueRepositioningDisabled,
serviceDeskEmailAddress,
canAdminIssues,
canEditProjectSettings,
@@ -53,6 +55,7 @@ export async function mountServiceDeskListApp() {
provide: {
releasesPath: projectDataReleasesPath,
autocompleteAwardEmojisPath: projectDataAutocompleteAwardEmojisPath,
+ hasBlockedIssuesFeature: parseBoolean(projectDataHasBlockedIssuesFeature),
hasIterationsFeature: parseBoolean(projectDataHasIterationsFeature),
hasIssueWeightsFeature: parseBoolean(projectDataHasIssueWeightsFeature),
hasIssuableHealthStatusFeature: parseBoolean(projectDataHasIssuableHealthStatusFeature),
@@ -72,6 +75,7 @@ export async function mountServiceDeskListApp() {
signInPath: projectDataSignInPath,
hasAnyIssues: parseBoolean(projectDataHasAnyIssues),
initialSort: projectDataInitialSort,
+ isIssueRepositioningDisabled: parseBoolean(projectDataIsIssueRepositioningDisabled),
},
render: (createComponent) => createComponent(ServiceDeskListApp),
});
diff --git a/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues.query.graphql
index c678b8dd8ab..d8cd28f5cf1 100644
--- a/app/assets/javascripts/service_desk/queries/get_service_desk_issues.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues.query.graphql
@@ -3,7 +3,6 @@
query getServiceDeskIssues(
$hideUsers: Boolean = false
- $isProject: Boolean = false
$isSignedIn: Boolean = false
$fullPath: ID!
$iid: String
@@ -22,8 +21,6 @@ query getServiceDeskIssues(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
- $crmContactId: String
- $crmOrganizationId: String
$not: NegatedIssueFilterInput
$or: UnionedIssueFilterInput
$beforeCursor: String
@@ -31,7 +28,7 @@ query getServiceDeskIssues(
$firstPageSize: Int
$lastPageSize: Int
) {
- project(fullPath: $fullPath) @include(if: $isProject) @persist {
+ project(fullPath: $fullPath) @persist {
id
issues(
iid: $iid
@@ -50,8 +47,6 @@ query getServiceDeskIssues(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
before: $beforeCursor
diff --git a/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql
index c2ba397d76f..008cde60b74 100644
--- a/app/assets/javascripts/service_desk/queries/get_service_desk_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/get_service_desk_issues_counts.query.graphql
@@ -1,5 +1,4 @@
query getServiceDeskIssuesCount(
- $isProject: Boolean = false
$fullPath: ID!
$iid: String
$search: String
@@ -14,12 +13,10 @@ query getServiceDeskIssuesCount(
$releaseTag: [String!]
$releaseTagWildcardId: ReleaseTagWildcardId
$types: [IssueType!]
- $crmContactId: String
- $crmOrganizationId: String
$not: NegatedIssueFilterInput
$or: UnionedIssueFilterInput
) {
- project(fullPath: $fullPath) @include(if: $isProject) {
+ project(fullPath: $fullPath) {
id
openedIssues: issues(
state: opened
@@ -36,8 +33,6 @@ query getServiceDeskIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
) {
@@ -58,8 +53,6 @@ query getServiceDeskIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
) {
@@ -80,8 +73,6 @@ query getServiceDeskIssuesCount(
releaseTag: $releaseTag
releaseTagWildcardId: $releaseTagWildcardId
types: $types
- crmContactId: $crmContactId
- crmOrganizationId: $crmOrganizationId
not: $not
or: $or
) {
diff --git a/app/assets/javascripts/service_desk/queries/issue.fragment.graphql b/app/assets/javascripts/issues/service_desk/queries/issue.fragment.graphql
index 3b49c0efb14..f72663ae5f6 100644
--- a/app/assets/javascripts/service_desk/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/issue.fragment.graphql
@@ -36,6 +36,7 @@ fragment IssueFragment on Issue {
username
webUrl
}
+ externalAuthor
labels {
nodes {
__persist
diff --git a/app/assets/javascripts/service_desk/queries/label.fragment.graphql b/app/assets/javascripts/issues/service_desk/queries/label.fragment.graphql
index bb1d8f1ac9b..bb1d8f1ac9b 100644
--- a/app/assets/javascripts/service_desk/queries/label.fragment.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/label.fragment.graphql
diff --git a/app/assets/javascripts/service_desk/queries/milestone.fragment.graphql b/app/assets/javascripts/issues/service_desk/queries/milestone.fragment.graphql
index 3cdf69bf585..3cdf69bf585 100644
--- a/app/assets/javascripts/service_desk/queries/milestone.fragment.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/milestone.fragment.graphql
diff --git a/app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql b/app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql
new file mode 100644
index 00000000000..2da7850d77d
--- /dev/null
+++ b/app/assets/javascripts/issues/service_desk/queries/reorder_service_desk_issues.mutation.graphql
@@ -0,0 +1,13 @@
+mutation reorderServiceDeskIssues(
+ $oldIndex: Int
+ $newIndex: Int
+ $namespace: String
+ $serializedVariables: String
+) {
+ reorderIssues(
+ oldIndex: $oldIndex
+ newIndex: $newIndex
+ namespace: $namespace
+ serializedVariables: $serializedVariables
+ ) @client
+}
diff --git a/app/assets/javascripts/service_desk/queries/search_project_labels.query.graphql b/app/assets/javascripts/issues/service_desk/queries/search_project_labels.query.graphql
index 89ce14134b4..89ce14134b4 100644
--- a/app/assets/javascripts/service_desk/queries/search_project_labels.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/search_project_labels.query.graphql
diff --git a/app/assets/javascripts/service_desk/queries/search_project_milestones.query.graphql b/app/assets/javascripts/issues/service_desk/queries/search_project_milestones.query.graphql
index f34166be87d..f34166be87d 100644
--- a/app/assets/javascripts/service_desk/queries/search_project_milestones.query.graphql
+++ b/app/assets/javascripts/issues/service_desk/queries/search_project_milestones.query.graphql
diff --git a/app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql b/app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql
new file mode 100644
index 00000000000..b01ae3863cd
--- /dev/null
+++ b/app/assets/javascripts/issues/service_desk/queries/set_sorting_preference.mutation.graphql
@@ -0,0 +1,5 @@
+mutation setSortingPreference($input: UserPreferencesUpdateInput!) {
+ userPreferencesUpdate(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/service_desk/search_tokens.js b/app/assets/javascripts/issues/service_desk/search_tokens.js
index 72750f518e4..72750f518e4 100644
--- a/app/assets/javascripts/service_desk/search_tokens.js
+++ b/app/assets/javascripts/issues/service_desk/search_tokens.js
diff --git a/app/assets/javascripts/service_desk/utils.js b/app/assets/javascripts/issues/service_desk/utils.js
index 86f76da3880..86f76da3880 100644
--- a/app/assets/javascripts/service_desk/utils.js
+++ b/app/assets/javascripts/issues/service_desk/utils.js
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 26c3db647a3..d59692d2a28 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,49 +1,36 @@
<script>
-import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import { createAlert } from '~/alert';
-import {
- issuableStatusText,
- STATUS_CLOSED,
- TYPE_EPIC,
- TYPE_INCIDENT,
- TYPE_ISSUE,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
+import { TYPE_EPIC, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
+import updateDescription from '~/issues/show/utils/update_description';
+import { sanitize } from '~/lib/dompurify';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
+import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-import { containsSensitiveToken, confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import Service from '../services/index';
-import Store from '../stores';
import DescriptionComponent from './description.vue';
import EditedComponent from './edited.vue';
import FormComponent from './form.vue';
import HeaderActions from './header_actions.vue';
import IssueHeader from './issue_header.vue';
import PinnedLinks from './pinned_links.vue';
+import StickyHeader from './sticky_header.vue';
import TitleComponent from './title.vue';
export default {
- WORKSPACE_PROJECT,
components: {
- GlIcon,
- GlBadge,
- GlIntersectionObserver,
HeaderActions,
IssueHeader,
TitleComponent,
EditedComponent,
FormComponent,
PinnedLinks,
- ConfidentialityBadge,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ StickyHeader,
},
props: {
author: {
@@ -234,21 +221,26 @@ export default {
},
},
data() {
- const store = new Store({
- titleHtml: this.initialTitleHtml,
- titleText: this.initialTitleText,
- descriptionHtml: this.initialDescriptionHtml,
- descriptionText: this.initialDescriptionText,
- updatedAt: this.updatedAt,
- updatedByName: this.updatedByName,
- updatedByPath: this.updatedByPath,
- taskCompletionStatus: this.initialTaskCompletionStatus,
- lock_version: this.lockVersion,
- });
-
return {
- store,
- state: store.state,
+ formState: {
+ title: '',
+ description: '',
+ lockedWarningVisible: false,
+ updateLoading: false,
+ lock_version: 0,
+ issuableTemplates: {},
+ },
+ state: {
+ titleHtml: this.initialTitleHtml,
+ titleText: this.initialTitleText,
+ descriptionHtml: this.initialDescriptionHtml,
+ descriptionText: this.initialDescriptionText,
+ updatedAt: this.updatedAt,
+ updatedByName: this.updatedByName,
+ updatedByPath: this.updatedByPath,
+ taskCompletionStatus: this.initialTaskCompletionStatus,
+ lock_version: this.lockVersion,
+ },
showForm: false,
templatesRequested: false,
isStickyHeaderShowing: false,
@@ -264,17 +256,9 @@ export default {
headerClasses() {
return this.issuableType === TYPE_INCIDENT ? 'gl-mb-3' : 'gl-mb-6';
},
- issuableTemplates() {
- return this.store.formState.issuableTemplates;
- },
- formState() {
- return this.store.formState;
- },
issueChanged() {
const {
- store: {
- formState: { description, title },
- },
+ formState: { description, title },
initialDescriptionText,
initialTitleText,
} = this;
@@ -292,26 +276,13 @@ export default {
defaultErrorMessage() {
return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
},
- isClosed() {
- return this.issuableStatus === STATUS_CLOSED;
- },
+
pinnedLinkClasses() {
return this.showTitleBorder
? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
: '';
},
- statusIcon() {
- if (this.issuableType === TYPE_EPIC) {
- return this.isClosed ? 'epic-closed' : 'epic';
- }
- return this.isClosed ? 'issue-closed' : 'issues';
- },
- statusVariant() {
- return this.isClosed ? 'info' : 'success';
- },
- statusText() {
- return issuableStatusText[this.issuableStatus];
- },
+
shouldShowStickyHeader() {
return [TYPE_INCIDENT, TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType);
},
@@ -322,7 +293,7 @@ export default {
this.poll = new Poll({
resource: this.service,
method: 'getData',
- successCallback: (res) => this.store.updateState(res.data),
+ successCallback: (res) => this.updateState(res.data),
errorCallback(err) {
throw new Error(err);
},
@@ -360,23 +331,37 @@ export default {
}
return undefined;
},
+ updateState(data) {
+ const stateShouldUpdate =
+ this.state.titleText !== data.title_text ||
+ this.state.descriptionText !== data.description_text;
- updateStoreState() {
+ if (stateShouldUpdate) {
+ this.formState.lockedWarningVisible = true;
+ }
+
+ Object.assign(this.state, convertObjectPropsToCamelCase(data));
+ // find if there is an open details node inside of the issue description.
+ const descriptionSection = document.body.querySelector(
+ '.detail-page-description.content-block',
+ );
+ const details =
+ descriptionSection != null && descriptionSection.getElementsByTagName('details');
+
+ this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
+ this.state.titleHtml = sanitize(data.title);
+ this.state.lock_version = data.lock_version;
+ },
+ refetchData() {
return this.service
.getData()
.then((res) => res.data)
- .then((data) => {
- this.store.updateState(data);
- })
- .catch(() => {
- createAlert({
- message: this.defaultErrorMessage,
- });
- });
+ .then(this.updateState)
+ .catch(() => createAlert({ message: this.defaultErrorMessage }));
},
setFormState(state) {
- this.store.setFormState(state);
+ this.formState = { ...this.formState, ...state };
},
updateFormState(templates = {}) {
@@ -416,7 +401,7 @@ export default {
this.templatesRequested = true;
this.requestTemplatesAndShowForm();
} else {
- this.updateAndShowForm(this.issuableTemplates);
+ this.updateAndShowForm(this.formState.issuableTemplates);
}
},
@@ -427,10 +412,7 @@ export default {
async updateIssuable() {
this.setFormState({ updateLoading: true });
- const {
- store: { formState },
- issueState,
- } = this;
+ const { formState, issueState } = this;
const issuablePayload = issueState.isDirty
? { ...formState, issue_type: issueState.issueType }
: formState;
@@ -464,7 +446,7 @@ export default {
visitUrl(URI);
}
})
- .then(this.updateStoreState)
+ .then(this.refetchData)
.then(() => {
eventHub.$emit('close.form');
})
@@ -518,7 +500,7 @@ export default {
this.poll.enable();
this.poll.makeDelayedRequest(POLLING_DELAY);
- this.updateStoreState();
+ this.refetchData();
},
},
};
@@ -531,7 +513,7 @@ export default {
:endpoint="endpoint"
:form-state="formState"
:initial-description-text="initialDescriptionText"
- :issuable-templates="issuableTemplates"
+ :issuable-templates="formState.issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
:project-path="projectPath"
@@ -559,61 +541,19 @@ export default {
</template>
</title-component>
- <gl-intersection-observer
+ <sticky-header
v-if="shouldShowStickyHeader"
- @appear="hideStickyHeader"
- @disappear="showStickyHeader"
- >
- <transition name="issuable-header-slide">
- <div
- v-if="isStickyHeaderShowing"
- 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="issue-sticky-header"
- >
- <div
- class="issue-sticky-header-text gl-display-flex gl-align-items-center gl-mx-auto gl-px-5"
- >
- <gl-badge :variant="statusVariant" class="gl-mr-2">
- <gl-icon :name="statusIcon" />
- <span class="gl-display-none gl-sm-display-block gl-ml-2">{{
- statusText
- }}</span></gl-badge
- >
- <span
- v-if="isLocked"
- v-gl-tooltip.bottom
- data-testid="locked"
- class="issuable-warning-icon"
- :title="__('This issue is locked. Only project members can comment.')"
- >
- <gl-icon name="lock" :aria-label="__('Locked')" />
- </span>
- <confidentiality-badge
- v-if="isConfidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
- :issuable-type="issuableType"
- />
- <span
- v-if="isHidden"
- v-gl-tooltip.bottom
- :title="__('This issue is hidden because its author has been banned')"
- data-testid="hidden"
- class="issuable-warning-icon"
- >
- <gl-icon name="spam" />
- </span>
- <a
- href="#top"
- class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0 gl-text-black-normal"
- :title="state.titleText"
- >
- {{ state.titleText }}
- </a>
- </div>
- </div>
- </transition>
- </gl-intersection-observer>
+ :is-confidential="isConfidential"
+ :is-hidden="isHidden"
+ :is-locked="isLocked"
+ :issuable-status="issuableStatus"
+ :issuable-type="issuableType"
+ :show="isStickyHeaderShowing"
+ :title="state.titleText"
+ :title-html="state.titleHtml"
+ @hide="hideStickyHeader"
+ @show="showStickyHeader"
+ />
<slot name="header">
<issue-header
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 90f01603f96..acbba216601 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -307,7 +307,8 @@ export default {
);
taskListItems?.forEach((item) => {
- const dropdown = this.createTaskListItemActions({ canUpdate: this.canUpdate });
+ const provide = { canUpdate: this.canUpdate, issuableType: this.issuableType };
+ const dropdown = this.createTaskListItemActions(provide);
this.insertNextToTaskListItemText(dropdown, item);
this.addPointerEventListeners(item, '.task-list-item-actions');
this.hasTaskListItemActions = true;
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 1ade5e654e9..81e5c30a264 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -79,62 +79,25 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin, glFeatureFlagMixin()],
- inject: {
- canCreateIssue: {
- default: false,
- },
- canDestroyIssue: {
- default: false,
- },
- canPromoteToEpic: {
- default: false,
- },
- canReopenIssue: {
- default: false,
- },
- canReportSpam: {
- default: false,
- },
- canUpdateIssue: {
- default: false,
- },
- iid: {
- default: '',
- },
- issuableId: {
- default: '',
- },
- isIssueAuthor: {
- default: false,
- },
- issuePath: {
- default: '',
- },
- issueType: {
- default: TYPE_ISSUE,
- },
- newIssuePath: {
- default: '',
- },
- projectPath: {
- default: '',
- },
- submitAsSpamPath: {
- default: '',
- },
- reportedUserId: {
- default: '',
- },
- reportedFromUrl: {
- default: '',
- },
- issuableEmailAddress: {
- default: '',
- },
- fullPath: {
- default: '',
- },
- },
+ inject: [
+ 'canCreateIssue',
+ 'canDestroyIssue',
+ 'canPromoteToEpic',
+ 'canReopenIssue',
+ 'canReportSpam',
+ 'canUpdateIssue',
+ 'iid',
+ 'isIssueAuthor',
+ 'issuePath',
+ 'issueType',
+ 'newIssuePath',
+ 'projectPath',
+ 'submitAsSpamPath',
+ 'reportedUserId',
+ 'reportedFromUrl',
+ 'issuableEmailAddress',
+ 'fullPath',
+ ],
data() {
return {
isReportAbuseDrawerOpen: false,
@@ -256,7 +219,7 @@ export default {
mutation: updateIssueMutation,
variables: {
input: {
- iid: this.iid.toString(),
+ iid: String(this.iid),
projectPath: this.projectPath,
stateEvent: this.isClosed ? ISSUE_STATE_EVENT_REOPEN : ISSUE_STATE_EVENT_CLOSE,
},
@@ -501,7 +464,7 @@ export default {
>{{ copyMailAddressText }}</gl-dropdown-item
>
</template>
- <gl-dropdown-divider v-if="showToggleIssueStateButton || canDestroyIssue || canReportSpam" />
+ <gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" />
<gl-dropdown-item
v-if="canReportSpam"
:href="submitAsSpamPath"
diff --git a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
index ac64c35bf15..ab1bb9253f4 100644
--- a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
@@ -107,10 +107,7 @@ export default {
</script>
<template>
- <div
- class="create-timeline-event gl-relative gl-display-flex gl-align-items-start"
- :class="{ 'timeline-entry-vertical-line': hasTimelineEvents }"
- >
+ <div class="create-timeline-event gl-relative gl-display-flex gl-align-items-start">
<div
v-if="hasTimelineEvents"
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-w-8 gl-h-8 gl-flex-shrink-0 gl-p-3 gl-z-index-1"
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index 4ec64ef838d..2909a4d2666 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -43,7 +43,7 @@ export default {
variables() {
return {
fullPath: this.fullPath,
- iid: this.iid,
+ iid: String(this.iid),
};
},
update(data) {
diff --git a/app/assets/javascripts/issues/show/components/sticky_header.vue b/app/assets/javascripts/issues/show/components/sticky_header.vue
new file mode 100644
index 00000000000..bcf10ee92bb
--- /dev/null
+++ b/app/assets/javascripts/issues/show/components/sticky_header.vue
@@ -0,0 +1,130 @@
+<script>
+import { GlBadge, GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import {
+ issuableStatusText,
+ STATUS_CLOSED,
+ TYPE_EPIC,
+ WORKSPACE_PROJECT,
+} from '~/issues/constants';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+export default {
+ WORKSPACE_PROJECT,
+ components: {
+ ConfidentialityBadge,
+ GlBadge,
+ GlIcon,
+ GlIntersectionObserver,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ props: {
+ isConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isHidden: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLocked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableStatus: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ },
+ show: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ titleHtml: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isClosed() {
+ return this.issuableStatus === STATUS_CLOSED;
+ },
+ statusIcon() {
+ if (this.issuableType === TYPE_EPIC) {
+ return this.isClosed ? 'epic-closed' : 'epic';
+ }
+ return this.isClosed ? 'issue-closed' : 'issues';
+ },
+ statusText() {
+ return issuableStatusText[this.issuableStatus];
+ },
+ statusVariant() {
+ return this.isClosed ? 'info' : 'success';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-intersection-observer @appear="$emit('hide')" @disappear="$emit('show')">
+ <transition name="issuable-header-slide">
+ <div
+ v-if="show"
+ 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="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"
+ >
+ <gl-badge :variant="statusVariant">
+ <gl-icon :name="statusIcon" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ statusText }}</span>
+ </gl-badge>
+ <span
+ v-if="isLocked"
+ v-gl-tooltip.bottom
+ data-testid="locked"
+ class="issuable-warning-icon"
+ :title="__('This issue is locked. Only project members can comment.')"
+ >
+ <gl-icon name="lock" :aria-label="__('Locked')" />
+ </span>
+ <confidentiality-badge
+ v-if="isConfidential"
+ :issuable-type="issuableType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ />
+ <span
+ v-if="isHidden"
+ v-gl-tooltip.bottom
+ :title="__('This issue is hidden because its author has been banned')"
+ data-testid="hidden"
+ class="issuable-warning-icon"
+ >
+ <gl-icon name="spam" />
+ </span>
+ <a
+ v-safe-html="titleHtml || title"
+ href="#top"
+ class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0 gl-text-black-normal"
+ >
+ </a>
+ </div>
+ </div>
+ </transition>
+ </gl-intersection-observer>
+</template>
diff --git a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
index 64b916caddb..55e2e857050 100644
--- a/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
+++ b/app/assets/javascripts/issues/show/components/task_list_item_actions.vue
@@ -1,5 +1,6 @@
<script>
import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { __, s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -13,7 +14,12 @@ export default {
GlDisclosureDropdown,
GlDisclosureDropdownItem,
},
- inject: ['canUpdate'],
+ inject: ['canUpdate', 'issuableType'],
+ computed: {
+ showConvertToTaskItem() {
+ return [TYPE_INCIDENT, TYPE_ISSUE].includes(this.issuableType);
+ },
+ },
methods: {
convertToTask() {
eventHub.$emit('convert-task-list-item', this.$el.closest('li').dataset.sourcepos);
@@ -37,12 +43,17 @@ export default {
text-sr-only
toggle-class="task-list-item-actions gl-opacity-0 gl-p-2! "
>
- <gl-disclosure-dropdown-item class="gl-ml-2!" @action="convertToTask">
+ <gl-disclosure-dropdown-item
+ v-if="showConvertToTaskItem"
+ class="gl-ml-2!"
+ data-testid="convert"
+ @action="convertToTask"
+ >
<template #list-item>
{{ $options.i18n.convertToTask }}
</template>
</gl-disclosure-dropdown-item>
- <gl-disclosure-dropdown-item class="gl-ml-2!" @action="deleteTaskListItem">
+ <gl-disclosure-dropdown-item class="gl-ml-2!" data-testid="delete" @action="deleteTaskListItem">
<template #list-item>
<span class="gl-text-red-500!">{{ $options.i18n.delete }}</span>
</template>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index a27f86bd9c3..b94f88f690e 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -6,13 +6,15 @@ import { apolloProvider } from '~/graphql_shared/issuable_client';
import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
+import initLinkedResources from '~/linked_resources';
import IssueApp from './components/app.vue';
-import HeaderActions from './components/header_actions.vue';
+import DescriptionComponent from './components/description.vue';
import IncidentTabs from './components/incidents/incident_tabs.vue';
import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
import { issueState } from './constants';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
import createRouter from './components/incidents/router';
+import { parseIssuableData } from './utils/parse_data';
const bootstrapApollo = (state = {}) => {
return apolloProvider.clients.defaultClient.cache.writeQuery({
@@ -23,14 +25,15 @@ const bootstrapApollo = (state = {}) => {
});
};
-export function initIncidentApp(issueData = {}, store) {
+export function initIssuableApp(store) {
const el = document.getElementById('js-issuable-app');
if (!el) {
return undefined;
}
- bootstrapApollo({ ...issueState, issueType: TYPE_INCIDENT });
+ const issuableData = parseIssuableData(el);
+ const headerActionsData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.headerActionsData));
const {
authorId,
@@ -38,137 +41,72 @@ export function initIncidentApp(issueData = {}, store) {
authorUsername,
authorWebUrl,
canCreateIncident,
- canUpdate,
- canUpdateTimelineEvent,
+ fullPath,
iid,
issuableId,
+ issueType,
+ hasIterationsFeature,
+ // for issue
+ registerPath,
+ signInPath,
+ // for incident
+ canUpdate,
+ canUpdateTimelineEvent,
currentPath,
currentTab,
- projectNamespace,
- projectPath,
- projectId,
hasLinkedAlerts,
+ projectId,
slaFeatureAvailable,
uploadMetricsFeatureAvailable,
- } = issueData;
- const headerActionsData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.headerActionsData));
-
- const fullPath = `${projectNamespace}/${projectPath}`;
- const router = createRouter(currentPath, currentTab);
-
- return new Vue({
- el,
- name: 'DescriptionRoot',
- apolloProvider,
- store,
- router,
- provide: {
- issueType: TYPE_INCIDENT,
- canCreateIncident,
- canUpdateTimelineEvent,
- canUpdate,
- fullPath,
- iid,
- issuableId,
- projectId,
- hasLinkedAlerts: parseBoolean(hasLinkedAlerts),
- slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
- uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
- contentEditorOnIssues: gon.features.contentEditorOnIssues,
- // for HeaderActions component
- canCreateIssue: parseBoolean(headerActionsData.canCreateIncident),
- canDestroyIssue: parseBoolean(headerActionsData.canDestroyIssue),
- canPromoteToEpic: parseBoolean(headerActionsData.canPromoteToEpic),
- canReopenIssue: parseBoolean(headerActionsData.canReopenIssue),
- canReportSpam: parseBoolean(headerActionsData.canReportSpam),
- canUpdateIssue: parseBoolean(headerActionsData.canUpdateIssue),
- isIssueAuthor: parseBoolean(headerActionsData.isIssueAuthor),
- issuePath: headerActionsData.issuePath,
- newIssuePath: headerActionsData.newIssuePath,
- projectPath: headerActionsData.projectPath,
- reportAbusePath: headerActionsData.reportAbusePath,
- reportedUserId: headerActionsData.reportedUserId,
- reportedFromUrl: headerActionsData.reportedFromUrl,
- submitAsSpamPath: headerActionsData.submitAsSpamPath,
- issuableEmailAddress: headerActionsData.issuableEmailAddress,
- },
- computed: {
- ...mapGetters(['getNoteableData']),
- },
- render(createElement) {
- return createElement(IssueApp, {
- props: {
- ...issueData,
- author: {
- id: authorId,
- name: authorName,
- username: authorUsername,
- webUrl: authorWebUrl,
- },
- issueId: Number(issuableId),
- issuableStatus: this.getNoteableData?.state,
- issuableType: TYPE_INCIDENT,
- descriptionComponent: IncidentTabs,
- showTitleBorder: false,
- isConfidential: this.getNoteableData?.confidential,
- },
- });
- },
- });
-}
-
-export function initIssueApp(issueData, store) {
- const el = document.getElementById('js-issuable-app');
+ } = issuableData;
- if (!el) {
- return undefined;
- }
+ const issueProvideData = { registerPath, signInPath };
+ const incidentProvideData = {
+ canUpdate,
+ canUpdateTimelineEvent,
+ hasLinkedAlerts: parseBoolean(hasLinkedAlerts),
+ projectId,
+ slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
+ uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
+ };
- const { fullPath, registerPath, signInPath } = el.dataset;
- const headerActionsData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.headerActionsData));
+ bootstrapApollo({ ...issueState, issueType });
scrollToTargetOnResize();
- bootstrapApollo({ ...issueState, issueType: TYPE_ISSUE });
-
- const {
- authorId,
- authorName,
- authorUsername,
- authorWebUrl,
- canCreateIncident,
- hasIssueWeightsFeature,
- hasIterationsFeature,
- ...issueProps
- } = issueData;
+ if (issueType === TYPE_INCIDENT) {
+ initLinkedResources();
+ }
return new Vue({
el,
name: 'DescriptionRoot',
apolloProvider,
store,
+ router: issueType === TYPE_INCIDENT ? createRouter(currentPath, currentTab) : undefined,
provide: {
canCreateIncident,
fullPath,
- registerPath,
- signInPath,
- hasIssueWeightsFeature,
+ iid,
+ issuableId,
+ issueType,
hasIterationsFeature,
+ ...(issueType === TYPE_ISSUE && issueProvideData),
+ ...(issueType === TYPE_INCIDENT && incidentProvideData),
// for HeaderActions component
- canCreateIssue: parseBoolean(headerActionsData.canCreateIssue),
+ canCreateIssue:
+ issueType === TYPE_INCIDENT
+ ? parseBoolean(headerActionsData.canCreateIncident)
+ : parseBoolean(headerActionsData.canCreateIssue),
canDestroyIssue: parseBoolean(headerActionsData.canDestroyIssue),
canPromoteToEpic: parseBoolean(headerActionsData.canPromoteToEpic),
canReopenIssue: parseBoolean(headerActionsData.canReopenIssue),
canReportSpam: parseBoolean(headerActionsData.canReportSpam),
canUpdateIssue: parseBoolean(headerActionsData.canUpdateIssue),
- iid: headerActionsData.iid,
- issuableId: headerActionsData.issuableId,
isIssueAuthor: parseBoolean(headerActionsData.isIssueAuthor),
issuePath: headerActionsData.issuePath,
- issueType: headerActionsData.issueType,
newIssuePath: headerActionsData.newIssuePath,
projectPath: headerActionsData.projectPath,
- projectId: headerActionsData.projectId,
reportAbusePath: headerActionsData.reportAbusePath,
reportedUserId: headerActionsData.reportedUserId,
reportedFromUrl: headerActionsData.reportedFromUrl,
@@ -181,67 +119,27 @@ export function initIssueApp(issueData, store) {
render(createElement) {
return createElement(IssueApp, {
props: {
- ...issueProps,
+ ...issuableData,
author: {
id: authorId,
name: authorName,
username: authorUsername,
webUrl: authorWebUrl,
},
+ descriptionComponent: issueType === TYPE_INCIDENT ? IncidentTabs : DescriptionComponent,
isConfidential: this.getNoteableData?.confidential,
isLocked: this.getNoteableData?.discussion_locked,
issuableStatus: this.getNoteableData?.state,
+ issuableType: issueType,
issueId: this.getNoteableData?.id,
issueIid: this.getNoteableData?.iid,
+ showTitleBorder: issueType !== TYPE_INCIDENT,
},
});
},
});
}
-export function initHeaderActions(store, type = '') {
- const el = document.querySelector('.js-issue-header-actions');
-
- if (!el) {
- return undefined;
- }
-
- bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
-
- const canCreate =
- type === TYPE_INCIDENT ? el.dataset.canCreateIncident : el.dataset.canCreateIssue;
-
- return new Vue({
- el,
- name: 'HeaderActionsRoot',
- apolloProvider,
- store,
- provide: {
- canCreateIssue: parseBoolean(canCreate),
- canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
- canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
- canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
- canReportSpam: parseBoolean(el.dataset.canReportSpam),
- canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
- iid: el.dataset.iid,
- issuableId: el.dataset.issuableId,
- isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
- issuePath: el.dataset.issuePath,
- issueType: el.dataset.issueType,
- newIssuePath: el.dataset.newIssuePath,
- projectPath: el.dataset.projectPath,
- projectId: el.dataset.projectId,
- reportAbusePath: el.dataset.reportAbusePath,
- reportedUserId: parseInt(el.dataset.reportedUserId, 10),
- reportedFromUrl: el.dataset.reportedFromUrl,
- submitAsSpamPath: el.dataset.submitAsSpamPath,
- issuableEmailAddress: el.dataset.issuableEmailAddress,
- fullPath: el.dataset.projectPath,
- },
- render: (createElement) => createElement(HeaderActions),
- });
-}
-
export function initSentryErrorStackTrace() {
const el = document.querySelector('#js-sentry-error-stack-trace');
diff --git a/app/assets/javascripts/issues/show/stores/index.js b/app/assets/javascripts/issues/show/stores/index.js
deleted file mode 100644
index a50913d3455..00000000000
--- a/app/assets/javascripts/issues/show/stores/index.js
+++ /dev/null
@@ -1,46 +0,0 @@
-import { sanitize } from '~/lib/dompurify';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import updateDescription from '../utils/update_description';
-
-export default class Store {
- constructor(initialState) {
- this.state = initialState;
- this.formState = {
- title: '',
- description: '',
- lockedWarningVisible: false,
- updateLoading: false,
- lock_version: 0,
- issuableTemplates: {},
- };
- }
-
- updateState(data) {
- if (this.stateShouldUpdate(data)) {
- this.formState.lockedWarningVisible = true;
- }
-
- Object.assign(this.state, convertObjectPropsToCamelCase(data));
- // find if there is an open details node inside of the issue description.
- const descriptionSection = document.body.querySelector(
- '.detail-page-description.content-block',
- );
- const details =
- descriptionSection != null && descriptionSection.getElementsByTagName('details');
-
- this.state.descriptionHtml = updateDescription(sanitize(data.description), details);
- this.state.titleHtml = sanitize(data.title);
- this.state.lock_version = data.lock_version;
- }
-
- stateShouldUpdate(data) {
- return (
- this.state.titleText !== data.title_text ||
- this.state.descriptionText !== data.description_text
- );
- }
-
- setFormState(state) {
- this.formState = Object.assign(this.formState, state);
- }
-}
diff --git a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
index 46c27c33f56..1d926c0d0c5 100644
--- a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue
@@ -193,6 +193,7 @@ export default {
>
<source-branch-dropdown
id="source-branch-select"
+ :key="selectedProject.id"
:selected-project="selectedProject"
:selected-branch-name="selectedSourceBranchName"
@change="onSourceBranchSelect"
diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
index dd9afb01590..52a12cc7771 100644
--- a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
@@ -98,13 +98,13 @@ export default {
:loading="initialProjectsLoading"
:searchable="true"
:searching="projectsLoading"
+ fluid-width
@search="onSearch"
@select="onProjectSelect"
>
<template #list-item="{ item: project }">
<gl-avatar-labeled
v-if="project"
- class="gl-text-truncate"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
:size="32"
:src="project.avatarUrl"
diff --git a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
index dac807dceb0..3f9dd4eb6c6 100644
--- a/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
@@ -2,6 +2,8 @@
import { GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
import { __ } from '~/locale';
+import { logError } from '~/lib/logger';
+
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
@@ -26,6 +28,7 @@ export default {
return {
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
+ sourceBranchNamesLoadingMore: false,
sourceBranchNames: [],
};
},
@@ -36,6 +39,11 @@ export default {
hasSelectedSourceBranch() {
return Boolean(this.selectedBranchName);
},
+ hasMoreBranches() {
+ return (
+ this.sourceBranchNames.length > 0 && this.sourceBranchNames.length % BRANCHES_PER_PAGE === 0
+ );
+ },
branchDropdownText() {
return this.selectedBranchName || __('Select a branch');
},
@@ -59,45 +67,63 @@ export default {
onSearch: debounce(function debouncedSearch(branchSearchQuery) {
this.onSourceBranchSearchQuery(branchSearchQuery);
}, 250),
- onSourceBranchSearchQuery(branchSearchQuery) {
+ async onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
- this.fetchSourceBranchNames({
+ this.sourceBranchNamesLoading = true;
+
+ await this.fetchSourceBranchNames({
+ projectPath: this.selectedProject.fullPath,
+ searchPattern: this.branchSearchQuery,
+ });
+ this.sourceBranchNamesLoading = false;
+ },
+ async onBottomReached() {
+ this.sourceBranchNamesLoadingMore = true;
+
+ await this.fetchSourceBranchNames({
projectPath: this.selectedProject.fullPath,
searchPattern: this.branchSearchQuery,
+ append: true,
});
+
+ this.sourceBranchNamesLoadingMore = false;
},
onError({ message } = {}) {
this.$emit('error', { message });
},
- async fetchSourceBranchNames({ projectPath, searchPattern } = {}) {
- this.sourceBranchNamesLoading = true;
+ async fetchSourceBranchNames({ projectPath, searchPattern, append = false } = {}) {
try {
const { data } = await this.$apollo.query({
query: getProjectQuery,
variables: {
projectPath,
branchNamesLimit: this.$options.BRANCHES_PER_PAGE,
- branchNamesOffset: 0,
+ branchNamesOffset: append ? this.sourceBranchNames.length : 0,
branchNamesSearchPattern: searchPattern ? `*${searchPattern}*` : '*',
},
});
const { branchNames, rootRef } = data?.project.repository || {};
- this.sourceBranchNames =
- branchNames.map((value) => {
+ const branchNameItems =
+ branchNames?.map((value) => {
return { text: value, value };
}) || [];
- // Use root ref as the default selection
- if (rootRef && !this.hasSelectedSourceBranch) {
- this.onSourceBranchSelect(rootRef);
+ if (append) {
+ this.sourceBranchNames.push(...branchNameItems);
+ } else {
+ this.sourceBranchNames = branchNameItems;
+
+ // Use root ref as the default selection
+ if (rootRef && !this.hasSelectedSourceBranch) {
+ this.onSourceBranchSelect(rootRef);
+ }
}
} catch (err) {
+ logError(err);
this.onError({
message: __('Something went wrong while fetching source branches.'),
});
- } finally {
- this.sourceBranchNamesLoading = false;
}
},
},
@@ -107,12 +133,17 @@ export default {
<template>
<gl-collapsible-listbox
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
+ :selected="selectedBranchName"
:disabled="!hasSelectedProject"
:items="sourceBranchNames"
:loading="initialSourceBranchNamesLoading"
:searchable="true"
:searching="sourceBranchNamesLoading"
:toggle-text="branchDropdownText"
+ fluid-width
+ :infinite-scroll="hasMoreBranches"
+ :infinite-scroll-loading="sourceBranchNamesLoadingMore"
+ @bottom-reached="onBottomReached"
@search="onSearch"
@select="onSourceBranchSelect"
/>
diff --git a/app/assets/javascripts/jira_connect/branches/index.js b/app/assets/javascripts/jira_connect/branches/index.js
index a9a56a6362e..893b9dfa1c7 100644
--- a/app/assets/javascripts/jira_connect/branches/index.js
+++ b/app/assets/javascripts/jira_connect/branches/index.js
@@ -19,6 +19,7 @@ export default function initJiraConnectBranches() {
return new Vue({
el,
+ name: 'JiraConnectNewBranchRoot',
apolloProvider,
provide: {
initialBranchName,
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index f8ca62da1a5..d737916857b 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -269,7 +269,7 @@ export default {
<gl-form-select
id="jira-project-select"
v-model="selectedProject"
- data-qa-selector="jira_project_dropdown"
+ data-testid="jira-project-dropdown"
class="mb-2"
:options="jiraProjects"
:state="selectState"
@@ -349,7 +349,7 @@ export default {
variant="confirm"
class="js-no-auto-disable"
:loading="isSubmitting"
- data-qa-selector="jira_issues_import_button"
+ data-testid="jira-issues-import-button"
>
{{ __('Continue') }}
</gl-button>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/constants.js b/app/assets/javascripts/jobs/components/filtered_search/constants.js
deleted file mode 100644
index 0daba892375..00000000000
--- a/app/assets/javascripts/jobs/components/filtered_search/constants.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export const jobStatusValues = [
- 'CANCELED',
- 'CREATED',
- 'FAILED',
- 'MANUAL',
- 'SUCCESS',
- 'PENDING',
- 'PREPARING',
- 'RUNNING',
- 'SCHEDULED',
- 'SKIPPED',
- 'WAITING_FOR_RESOURCE',
-];
diff --git a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
deleted file mode 100644
index 67cdca6aa0a..00000000000
--- a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<script>
-import { GlFilteredSearch } from '@gitlab/ui';
-import {
- OPERATORS_IS,
- TOKEN_TITLE_STATUS,
- TOKEN_TYPE_STATUS,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import JobStatusToken from './tokens/job_status_token.vue';
-
-export default {
- components: {
- GlFilteredSearch,
- },
- props: {
- queryString: {
- type: Object,
- required: false,
- default: null,
- },
- },
- computed: {
- tokens() {
- return [
- {
- type: TOKEN_TYPE_STATUS,
- icon: 'status',
- title: TOKEN_TITLE_STATUS,
- unique: true,
- token: JobStatusToken,
- operators: OPERATORS_IS,
- },
- ];
- },
- filteredSearchValue() {
- if (this.queryString?.statuses) {
- return [
- {
- type: TOKEN_TYPE_STATUS,
- value: {
- data: this.queryString?.statuses,
- operator: '=',
- },
- },
- ];
- }
- return [];
- },
- },
- methods: {
- onSubmit(filters) {
- this.$emit('filterJobsBySearch', filters);
- },
- },
-};
-</script>
-
-<template>
- <gl-filtered-search
- :placeholder="s__('Jobs|Filter jobs')"
- :available-tokens="tokens"
- :value="filteredSearchValue"
- @submit="onSubmit"
- />
-</template>
diff --git a/app/assets/javascripts/jobs/components/filtered_search/utils.js b/app/assets/javascripts/jobs/components/filtered_search/utils.js
deleted file mode 100644
index 696cd8d4706..00000000000
--- a/app/assets/javascripts/jobs/components/filtered_search/utils.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { jobStatusValues } from './constants';
-
-// validates query string used for filtered search
-// on jobs table to ensure GraphQL query is called correctly
-export const validateQueryString = (queryStringObj) => {
- // currently only one token is supported `statuses`
- // this code will need to be expanded as more tokens
- // are introduced
-
- const filters = Object.keys(queryStringObj);
-
- if (filters.includes('statuses')) {
- const queryStringStatus = {
- statuses: queryStringObj.statuses.toUpperCase(),
- };
-
- const found = jobStatusValues.find((status) => status === queryStringStatus.statuses);
-
- if (found) {
- return queryStringStatus;
- }
-
- return null;
- }
-
- return null;
-};
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
deleted file mode 100644
index 7f25ca8a94d..00000000000
--- a/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { GlLink } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-export default {
- components: {
- ClipboardButton,
- GlLink,
- },
- props: {
- commit: {
- type: Object,
- required: true,
- },
- mergeRequest: {
- type: Object,
- required: false,
- default: null,
- },
- },
-};
-</script>
-<template>
- <div>
- <span class="gl-font-weight-bold">{{ __('Commit') }}</span>
-
- <gl-link :href="commit.commit_path" class="gl-text-blue-600!" data-testid="commit-sha">
- {{ commit.short_id }}
- </gl-link>
-
- <clipboard-button
- :text="commit.id"
- :title="__('Copy commit SHA')"
- category="tertiary"
- size="small"
- />
-
- <span v-if="mergeRequest">
- {{ __('in') }}
- <gl-link :href="mergeRequest.path" class="gl-text-blue-600!" data-testid="link-commit"
- >!{{ mergeRequest.iid }}</gl-link
- >
- </span>
-
- <p class="gl-mb-0">{{ commit.title }}</p>
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
deleted file mode 100644
index 40b3de7edd9..00000000000
--- a/app/assets/javascripts/jobs/constants.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { __, s__ } from '~/locale';
-
-const cancel = __('Cancel');
-const moreInfo = __('More information');
-
-export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
-
-export const JOB_SIDEBAR_COPY = {
- cancel,
- cancelJobButtonLabel: s__('Job|Cancel'),
- debug: __('Debug'),
- eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
- eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
- newIssue: __('New issue'),
- retryJobLabel: s__('Job|Retry'),
- toggleSidebar: __('Toggle Sidebar'),
- runAgainJobButtonLabel: s__('Job|Run again'),
- updateVariables: s__('Job|Update CI/CD variables'),
-};
-
-export const JOB_GRAPHQL_ERRORS = {
- jobMutationErrorText: __('There was an error running the job. Please try again.'),
- jobQueryErrorText: __('There was an error fetching the job.'),
-};
-
-export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
- cancel,
- info: s__(
- `Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment.
- Retrying this job could result in overwriting the environment with the older source code.`,
- ),
- areYouSure: s__('Jobs|Are you sure you want to proceed?'),
- moreInfo,
- primaryText: __('Retry job'),
- title: s__('Jobs|Are you sure you want to retry this job?'),
-};
-
-export const SUCCESS_STATUS = 'SUCCESS';
-export const PASSED_STATUS = 'passed';
-export const MANUAL_STATUS = 'manual';
diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 587cc82f0fa..db1dbc72624 100644
--- a/app/assets/javascripts/labels/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -276,7 +276,8 @@ export default class LabelsSelect {
if (selected && selected.id === 0) {
this.selected = [];
return __('No label');
- } else if (isSelected) {
+ }
+ if (isSelected) {
this.selected.push(title);
} else if (!isSelected && title) {
const index = this.selected.indexOf(title);
@@ -285,7 +286,8 @@ export default class LabelsSelect {
if (selectedLabels.length === 1) {
return selectedLabels;
- } else if (selectedLabels.length) {
+ }
+ if (selectedLabels.length) {
return sprintf(__('%{firstLabel} +%{labelCount} more'), {
firstLabel: selectedLabels[0],
labelCount: selectedLabels.length - 1,
diff --git a/app/assets/javascripts/lib/swagger.js b/app/assets/javascripts/lib/swagger.js
index fcdab18c623..b0701054174 100644
--- a/app/assets/javascripts/lib/swagger.js
+++ b/app/assets/javascripts/lib/swagger.js
@@ -27,7 +27,7 @@ const renderSwaggerUI = (value) => {
spec,
dom_id: '#swagger-ui',
deepLinking: true,
- displayOperationId: true,
+ displayOperationId: Boolean(getParameterByName('displayOperationId')),
});
})
.catch((error) => {
diff --git a/app/assets/javascripts/lib/utils/array_utility.js b/app/assets/javascripts/lib/utils/array_utility.js
index 04f9cb1cdb5..9eddae860c2 100644
--- a/app/assets/javascripts/lib/utils/array_utility.js
+++ b/app/assets/javascripts/lib/utils/array_utility.js
@@ -28,3 +28,18 @@ export const swapArrayItems = (array, leftIndex = 0, rightIndex = 0) => {
export const getDuplicateItemsFromArray = (array) => [
...new Set(array.filter((value, index) => array.indexOf(value) !== index)),
];
+
+/**
+ * Toggles the presence of an item in a given array.
+ * Use pass by reference when toggling non-primivive types.
+ *
+ * @param {Array} array - The array to use
+ * @param {Any} item - The array item to toggle
+ * @returns {Array} new array with toggled item
+ */
+export const toggleArrayItem = (array, value) => {
+ if (array.includes(value)) {
+ return array.filter((item) => item !== value);
+ }
+ return [...array, value];
+};
diff --git a/app/assets/javascripts/lib/utils/breadcrumbs.js b/app/assets/javascripts/lib/utils/breadcrumbs.js
new file mode 100644
index 00000000000..e38094fc895
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/breadcrumbs.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+
+// TODO: Review replacing this when a breadcrumbs ViewComponent has been created https://gitlab.com/gitlab-org/gitlab/-/issues/367326
+export const injectVueAppBreadcrumbs = (router, BreadcrumbsComponent, apolloProvider = null) => {
+ const breadcrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
+
+ if (breadcrumbEls.length < 1) {
+ return false;
+ }
+
+ const breadcrumbEl = breadcrumbEls[breadcrumbEls.length - 1];
+
+ const lastCrumb = breadcrumbEl.children[0];
+ const nestedBreadcrumbEl = document.createElement('div');
+
+ breadcrumbEl.replaceChild(nestedBreadcrumbEl, lastCrumb);
+
+ return new Vue({
+ el: nestedBreadcrumbEl,
+ router,
+ apolloProvider,
+ render(createElement) {
+ return createElement(BreadcrumbsComponent, {
+ class: breadcrumbEl.className,
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index cca4cf68f5e..7d16af003e4 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -86,7 +86,6 @@ export const handleLocationHash = () => {
const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
const diffFileHeader = document.querySelector('.js-file-title');
- const versionMenusContainer = document.querySelector('.mr-version-menus-container');
const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
let adjustment = 0;
@@ -97,7 +96,6 @@ export const handleLocationHash = () => {
adjustment -= getElementOffsetHeight(fixedTopBar);
adjustment -= getElementOffsetHeight(performanceBar);
adjustment -= getElementOffsetHeight(diffFileHeader);
- adjustment -= getElementOffsetHeight(versionMenusContainer);
if (isInIssuePage()) {
adjustment -= getElementOffsetHeight(fixedIssuableTitle);
@@ -731,3 +729,11 @@ export const isCurrentUser = (userId) => {
return Number(userId) === currentUserId;
};
+
+/**
+ * Clones an object via JSON stringifying and re-parsing.
+ * This ensures object references are not persisted (e.g. unlike lodash cloneDeep)
+ */
+export const cloneWithoutReferences = (obj) => {
+ return JSON.parse(JSON.stringify(obj));
+};
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index aceae188b73..da5fb831ae5 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -3,15 +3,6 @@ export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
export const THOUSAND = 1000;
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
-
-export const DATETIME_RANGE_TYPES = {
- fixed: 'fixed',
- anchored: 'anchored',
- rolling: 'rolling',
- open: 'open',
- invalid: 'invalid',
-};
-
export const BV_SHOW_MODAL = 'bv::show::modal';
export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
index d52672b9d08..4e0d19f2c2a 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -133,7 +133,8 @@ export const dayInQuarter = (date, quarter) => {
return quarter.reduce((acc, month) => {
if (dateValues.month > month.getMonth()) {
return acc + totalDaysInMonth(month);
- } else if (dateValues.month === month.getMonth()) {
+ }
+ if (dateValues.month === month.getMonth()) {
return acc + dateValues.date;
}
return acc + 0;
@@ -562,9 +563,11 @@ export const approximateDuration = (seconds = 0) => {
if (seconds < 30) {
return __('less than a minute');
- } else if (seconds < MINUTES_LIMIT) {
+ }
+ if (seconds < MINUTES_LIMIT) {
return n__('1 minute', '%d minutes', seconds < ONE_MINUTE_LIMIT ? 1 : minutes);
- } else if (seconds < HOURS_LIMIT) {
+ }
+ if (seconds < HOURS_LIMIT) {
return n__('about 1 hour', 'about %d hours', seconds < ONE_HOUR_LIMIT ? 1 : hours);
}
return n__('1 day', '%d days', seconds < ONE_DAY_LIMIT ? 1 : days);
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index b0264796d90..c4b8f95e99f 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -93,10 +93,12 @@ export const humanizeTimeInterval = (intervalInSeconds) => {
if (intervalInSeconds < 60 /* = 1 minute */) {
const seconds = Math.round(intervalInSeconds * 10) / 10;
return n__('%d second', '%d seconds', seconds);
- } else if (intervalInSeconds < 3600 /* = 1 hour */) {
+ }
+ if (intervalInSeconds < 3600 /* = 1 hour */) {
const minutes = Math.round(intervalInSeconds / 6) / 10;
return n__('%d minute', '%d minutes', minutes);
- } else if (intervalInSeconds < 86400 /* = 1 day */) {
+ }
+ if (intervalInSeconds < 86400 /* = 1 day */) {
const hours = Math.round(intervalInSeconds / 360) / 10;
return n__('%d hour', '%d hours', hours);
}
@@ -378,19 +380,24 @@ export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, mont
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
value: roundToNearestHalf(months),
});
- } else if (weeks) {
+ }
+ if (weeks) {
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
value: roundToNearestHalf(weeks),
});
- } else if (days) {
+ }
+ if (days) {
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
value: roundToNearestHalf(days),
});
- } else if (hours) {
+ }
+ if (hours) {
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
- } else if (minutes) {
+ }
+ if (minutes) {
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
- } else if (seconds) {
+ }
+ if (seconds) {
return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
}
return '-';
@@ -441,11 +448,13 @@ export const humanTimeframe = (startDate, dueDate) => {
startDate: startDateInWords,
dueDate: dueDateInWords,
});
- } else if (startDate && !dueDate) {
+ }
+ if (startDate && !dueDate) {
return sprintf(__('%{startDate} – No due date'), {
startDate: dateInWords(start, true, false),
});
- } else if (!startDate && dueDate) {
+ }
+ if (!startDate && dueDate) {
return sprintf(__('No start date – %{dueDate}'), {
dueDate: dateInWords(due, true, false),
});
diff --git a/app/assets/javascripts/lib/utils/datetime_range.js b/app/assets/javascripts/lib/utils/datetime_range.js
index 548f5a438df..f0a03874949 100644
--- a/app/assets/javascripts/lib/utils/datetime_range.js
+++ b/app/assets/javascripts/lib/utils/datetime_range.js
@@ -1,27 +1,6 @@
-import { pick, omit, isEqual, isEmpty } from 'lodash';
import dateformat from '~/lib/dateformat';
-import { DATETIME_RANGE_TYPES } from './constants';
-import { secondsToMilliseconds } from './datetime_utility';
-const MINIMUM_DATE = new Date(0);
-
-const DEFAULT_DIRECTION = 'before';
-
-const durationToMillis = (duration) => {
- if (Object.entries(duration).length === 1 && Number.isFinite(duration.seconds)) {
- return secondsToMilliseconds(duration.seconds);
- }
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Invalid duration: only `seconds` is supported');
-};
-
-const dateMinusDuration = (date, duration) => new Date(date.getTime() - durationToMillis(duration));
-
-const datePlusDuration = (date, duration) => new Date(date.getTime() + durationToMillis(duration));
-
-const isValidDuration = (duration) => Boolean(duration && Number.isFinite(duration.seconds));
-
-const isValidDateString = (dateString) => {
+export const isValidDateString = (dateString) => {
if (typeof dateString !== 'string' || !dateString.trim()) {
return false;
}
@@ -38,291 +17,3 @@ const isValidDateString = (dateString) => {
}
return !Number.isNaN(Date.parse(isoFormatted));
};
-
-const handleRangeDirection = ({ direction = DEFAULT_DIRECTION, anchorDate, minDate, maxDate }) => {
- let startDate;
- let endDate;
-
- if (direction === DEFAULT_DIRECTION) {
- startDate = minDate;
- endDate = anchorDate;
- } else {
- startDate = anchorDate;
- endDate = maxDate;
- }
-
- return {
- startDate,
- endDate,
- };
-};
-
-/**
- * Converts a fixed range to a fixed range
- * @param {Object} fixedRange - A range with fixed start and
- * end (e.g. "midnight January 1st 2020 to midday January31st 2020")
- */
-const convertFixedToFixed = ({ start, end }) => ({
- start,
- end,
-});
-
-/**
- * Converts an anchored range to a fixed range
- * @param {Object} anchoredRange - A duration of time
- * relative to a fixed point in time (e.g., "the 30 minutes
- * before midnight January 1st 2020", or "the 2 days
- * after midday on the 11th of May 2019")
- */
-const convertAnchoredToFixed = ({ anchor, duration, direction }) => {
- const anchorDate = new Date(anchor);
-
- const { startDate, endDate } = handleRangeDirection({
- minDate: dateMinusDuration(anchorDate, duration),
- maxDate: datePlusDuration(anchorDate, duration),
- direction,
- anchorDate,
- });
-
- return {
- start: startDate.toISOString(),
- end: endDate.toISOString(),
- };
-};
-
-/**
- * Converts a rolling change to a fixed range
- *
- * @param {Object} rollingRange - A time range relative to
- * now (e.g., "last 2 minutes", or "next 2 days")
- */
-const convertRollingToFixed = ({ duration, direction }) => {
- // Use Date.now internally for easier mocking in tests
- const now = new Date(Date.now());
-
- return convertAnchoredToFixed({
- duration,
- direction,
- anchor: now.toISOString(),
- });
-};
-
-/**
- * Converts an open range to a fixed range
- *
- * @param {Object} openRange - A time range relative
- * to an anchor (e.g., "before midnight on the 1st of
- * January 2020", or "after midday on the 11th of May 2019")
- */
-const convertOpenToFixed = ({ anchor, direction }) => {
- // Use Date.now internally for easier mocking in tests
- const now = new Date(Date.now());
-
- const { startDate, endDate } = handleRangeDirection({
- minDate: MINIMUM_DATE,
- maxDate: now,
- direction,
- anchorDate: new Date(anchor),
- });
-
- return {
- start: startDate.toISOString(),
- end: endDate.toISOString(),
- };
-};
-
-/**
- * Handles invalid date ranges
- */
-const handleInvalidRange = () => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('The input range does not have the right format.');
-};
-
-const handlers = {
- invalid: handleInvalidRange,
- fixed: convertFixedToFixed,
- anchored: convertAnchoredToFixed,
- rolling: convertRollingToFixed,
- open: convertOpenToFixed,
-};
-
-/**
- * Validates and returns the type of range
- *
- * @param {Object} Date time range
- * @returns {String} `key` value for one of the handlers
- */
-export function getRangeType(range) {
- const { start, end, anchor, duration } = range;
-
- if ((start || end) && !anchor && !duration) {
- return isValidDateString(start) && isValidDateString(end)
- ? DATETIME_RANGE_TYPES.fixed
- : DATETIME_RANGE_TYPES.invalid;
- }
- if (anchor && duration) {
- return isValidDateString(anchor) && isValidDuration(duration)
- ? DATETIME_RANGE_TYPES.anchored
- : DATETIME_RANGE_TYPES.invalid;
- }
- if (duration && !anchor) {
- return isValidDuration(duration) ? DATETIME_RANGE_TYPES.rolling : DATETIME_RANGE_TYPES.invalid;
- }
- if (anchor && !duration) {
- return isValidDateString(anchor) ? DATETIME_RANGE_TYPES.open : DATETIME_RANGE_TYPES.invalid;
- }
- return DATETIME_RANGE_TYPES.invalid;
-}
-
-/**
- * convertToFixedRange Transforms a `range of time` into a `fixed range of time`.
- *
- * The following types of a `ranges of time` can be represented:
- *
- * Fixed Range: A range with fixed start and end (e.g. "midnight January 1st 2020 to midday January 31st 2020")
- * Anchored Range: A duration of time relative to a fixed point in time (e.g., "the 30 minutes before midnight January 1st 2020", or "the 2 days after midday on the 11th of May 2019")
- * Rolling Range: A time range relative to now (e.g., "last 2 minutes", or "next 2 days")
- * Open Range: A time range relative to an anchor (e.g., "before midnight on the 1st of January 2020", or "after midday on the 11th of May 2019")
- *
- * @param {Object} dateTimeRange - A Time Range representation
- * It contains the data needed to create a fixed time range plus
- * a label (recommended) to indicate the range that is covered.
- *
- * A definition via a TypeScript notation is presented below:
- *
- *
- * type Duration = { // A duration of time, always in seconds
- * seconds: number;
- * }
- *
- * type Direction = 'before' | 'after'; // Direction of time relative to an anchor
- *
- * type FixedRange = {
- * start: ISO8601;
- * end: ISO8601;
- * label: string;
- * }
- *
- * type AnchoredRange = {
- * anchor: ISO8601;
- * duration: Duration;
- * direction: Direction; // defaults to 'before'
- * label: string;
- * }
- *
- * type RollingRange = {
- * duration: Duration;
- * direction: Direction; // defaults to 'before'
- * label: string;
- * }
- *
- * type OpenRange = {
- * anchor: ISO8601;
- * direction: Direction; // defaults to 'before'
- * label: string;
- * }
- *
- * type DateTimeRange = FixedRange | AnchoredRange | RollingRange | OpenRange;
- *
- *
- * @returns {FixedRange} An object with a start and end in ISO8601 format.
- */
-export const convertToFixedRange = (dateTimeRange) =>
- handlers[getRangeType(dateTimeRange)](dateTimeRange);
-
-/**
- * Returns a copy of the object only with time range
- * properties relevant to time range calculation.
- *
- * Filtered properties are:
- * - 'start'
- * - 'end'
- * - 'anchor'
- * - 'duration'
- * - 'direction': if direction is already the default, its removed.
- *
- * @param {Object} timeRange - A time range object
- * @returns Copy of time range
- */
-const pruneTimeRange = (timeRange) => {
- const res = pick(timeRange, ['start', 'end', 'anchor', 'duration', 'direction']);
- if (res.direction === DEFAULT_DIRECTION) {
- return omit(res, 'direction');
- }
- return res;
-};
-
-/**
- * Returns true if the time ranges are equal according to
- * the time range calculation properties
- *
- * @param {Object} timeRange - A time range object
- * @param {Object} other - Time range object to compare with.
- * @returns true if the time ranges are equal, false otherwise
- */
-export const isEqualTimeRanges = (timeRange, other) => {
- const tr1 = pruneTimeRange(timeRange);
- const tr2 = pruneTimeRange(other);
- return isEqual(tr1, tr2);
-};
-
-/**
- * Searches for a time range in a array of time ranges using
- * only the properies relevant to time ranges calculation.
- *
- * @param {Object} timeRange - Time range to search (needle)
- * @param {Array} timeRanges - Array of time tanges (haystack)
- */
-export const findTimeRange = (timeRange, timeRanges) =>
- timeRanges.find((element) => isEqualTimeRanges(element, timeRange));
-
-// Time Ranges as URL Parameters Utils
-
-/**
- * List of possible time ranges parameters
- */
-export const timeRangeParamNames = ['start', 'end', 'anchor', 'duration_seconds', 'direction'];
-
-/**
- * Converts a valid time range to a flat key-value pairs object.
- *
- * Duration is flatted to avoid having nested objects.
- *
- * @param {Object} A time range
- * @returns key-value pairs object that can be used as parameters in a URL.
- */
-export const timeRangeToParams = (timeRange) => {
- let params = pruneTimeRange(timeRange);
- if (timeRange.duration) {
- const durationParms = {};
- Object.keys(timeRange.duration).forEach((key) => {
- durationParms[`duration_${key}`] = timeRange.duration[key].toString();
- });
- params = { ...durationParms, ...params };
- params = omit(params, 'duration');
- }
- return params;
-};
-
-/**
- * Converts a valid set of flat params to a time range object
- *
- * Parameters that are not part of time range object are ignored.
- *
- * @param {params} params - key-value pairs object.
- */
-export const timeRangeFromParams = (params) => {
- const timeRangeParams = pick(params, timeRangeParamNames);
- let range = Object.entries(timeRangeParams).reduce((acc, [key, val]) => {
- // unflatten duration
- if (key.startsWith('duration_')) {
- acc.duration = acc.duration || {};
- acc.duration[key.slice('duration_'.length)] = parseInt(val, 10);
- return acc;
- }
- return { [key]: val, ...acc };
- }, {});
- range = pruneTimeRange(range);
- return !isEmpty(range) ? range : null;
-};
diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js
index 6d6361d19b6..64ba151d829 100644
--- a/app/assets/javascripts/lib/utils/grammar.js
+++ b/app/assets/javascripts/lib/utils/grammar.js
@@ -19,9 +19,11 @@ import { sprintf, s__ } from '~/locale';
export const toNounSeriesText = (items, { onlyCommas = false } = {}) => {
if (items.length === 0) {
return '';
- } else if (items.length === 1) {
+ }
+ if (items.length === 1) {
return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false);
- } else if (items.length === 2 && !onlyCommas) {
+ }
+ if (items.length === 2 && !onlyCommas) {
return sprintf(
s__('nounSeries|%{firstItem} and %{lastItem}'),
{
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 0e943cdb623..d17719c0bc0 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -84,9 +84,11 @@ export function numberToHumanSizeSplit(size, digits = 2) {
if (abs < BYTES_IN_KIB) {
return [size.toString(), BYTES_FORMAT_BYTES];
- } else if (abs < BYTES_IN_KIB ** 2) {
+ }
+ if (abs < BYTES_IN_KIB ** 2) {
return [bytesToKiB(size).toFixed(digits), BYTES_FORMAT_KIB];
- } else if (abs < BYTES_IN_KIB ** 3) {
+ }
+ if (abs < BYTES_IN_KIB ** 3) {
return [bytesToMiB(size).toFixed(digits), BYTES_FORMAT_MIB];
}
return [bytesToGiB(size).toFixed(digits), BYTES_FORMAT_GIB];
diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js
index 8e673855631..49de7b3a081 100644
--- a/app/assets/javascripts/lib/utils/secret_detection.js
+++ b/app/assets/javascripts/lib/utils/secret_detection.js
@@ -24,6 +24,10 @@ export const containsSensitiveToken = (message) => {
name: 'Feed Token',
regex: 'feed_token=((glft-)?[0-9a-zA-Z_-]{20}|glft-[a-h0-9]+-[0-9]+_)',
},
+ {
+ name: 'GitLab OAuth Application Secret',
+ regex: `gloas-[0-9a-zA-Z_-]{64}`,
+ },
];
for (const rule of sensitiveDataPatterns) {
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index e6eb74834c0..d48e0217fd8 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -202,7 +202,8 @@ function moveCursor({
const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
- } else if (editor) {
+ }
+ if (editor) {
editor.selectWithinSelection(select, tag);
return;
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 31e16f7b4db..638ee1f7e5a 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -178,18 +178,6 @@ export function capitalizeFirstCharacter(text) {
}
/**
- * Returns the first character capitalized
- *
- * If falsey, returns empty string.
- *
- * @param {String} text
- * @return {String}
- */
-export function getFirstCharacterCapitalized(text) {
- return text ? text.charAt(0).toUpperCase() : '';
-}
-
-/**
* Replaces all html tags from a string with the given replacement.
*
* @param {String} string
@@ -529,9 +517,11 @@ export const humanizeBranchValidationErrors = (invalidChars = []) => {
if (chars.length && !chars.includes(' ')) {
return sprintf(__("Can't contain %{chars}"), { chars: chars.join(', ') });
- } else if (chars.includes(' ') && chars.length <= 1) {
+ }
+ if (chars.includes(' ') && chars.length <= 1) {
return __("Can't contain spaces");
- } else if (chars.includes(' ') && chars.length > 1) {
+ }
+ if (chars.includes(' ') && chars.length > 1) {
return sprintf(__("Can't contain spaces, %{chars}"), {
chars: chars.filter((c) => c !== ' ').join(', '),
});
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 08c98298121..ea0520e3157 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,5 +1,3 @@
-import * as Sentry from '@sentry/browser';
-
export const DASH_SCOPE = '-';
export const PATH_SEPARATOR = '/';
@@ -705,11 +703,7 @@ export function visitUrl(destination, external = false) {
}
if (!isSafeURL(url)) {
- // For now log this to Sentry and do not block the execution.
- // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121551#note_1408873600
- // for more context. Once we're sure that it's not breaking functionality, we can use
- // a RangeError here (throw new RangeError('Only http and https protocols are allowed')).
- Sentry.captureException(new RangeError(`Only http and https protocols are allowed: ${url}`));
+ throw new RangeError(`Only http and https protocols are allowed: ${url}`);
}
if (external) {
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 38d2f3d7551..f1e87145406 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -5,10 +5,11 @@ import { joinPaths } from '~/lib/utils/url_utility';
* See https://gitlab.com/gitlab-org/gitlab/-/issues/321656 for a fix
*/
export function resetServiceWorkersPublicPath() {
+ // No-op if we're running Vite instead of Webpack
+ if (typeof __webpack_public_path__ === 'undefined') return; // eslint-disable-line camelcase
// __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.
// see: https://webpack.js.org/guides/public-path/
const relativeRootPath = (gon && gon.relative_url_root) || '';
- const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/');
- __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase
+ __webpack_public_path__ = joinPaths(relativeRootPath, '/assets/webpack/'); // eslint-disable-line camelcase
}
diff --git a/app/assets/javascripts/locale/ensure_single_line.cjs b/app/assets/javascripts/locale/ensure_single_line.cjs
index f7790cadc48..abdd56c3589 100644
--- a/app/assets/javascripts/locale/ensure_single_line.cjs
+++ b/app/assets/javascripts/locale/ensure_single_line.cjs
@@ -1,15 +1,11 @@
const SPLIT_REGEX = /\s*[\r\n]+\s*/;
/**
- *
- * strips newlines from strings and replaces them with a single space
- *
+ * Strips newlines from strings and replaces them with a single space.
* @example
- *
* ensureSingleLine('foo \n bar') === 'foo bar'
- *
- * @param {String} str
- * @returns {String}
+ * @param {string} - str
+ * @returns {string}
*/
module.exports = function ensureSingleLine(str) {
// This guard makes the function significantly faster
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 600654794a5..48cf06e0793 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -19,22 +19,22 @@ if (hasTranslations) {
}
/**
- Translates `text`
- @param text The text to be translated
- @returns {String} The translated text
-*/
+ * Translates `text`.
+ * @param {string} text - The text to be translated
+ * @returns {string} The translated text
+ */
const gettext = (text) => locale.gettext(ensureSingleLine(text));
/**
- Translate the text with a number
- if the number is more than 1 it will use the `pluralText` translation.
- This method allows for contexts, see below re. contexts
-
- @param text Singular text to translate (eg. '%d day')
- @param pluralText Plural text to translate (eg. '%d days')
- @param count Number to decide which translation to use (eg. 2)
- @returns {String} Translated text with the number replaced (eg. '2 days')
-*/
+ * Translate the text with a number.
+ *
+ * If the number is more than 1 it will use the `pluralText` translation.
+ * This method allows for contexts, see below re. contexts
+ * @param {string} text - Singular text to translate (e.g. '%d day')
+ * @param {string} pluralText - Plural text to translate (e.g. '%d days')
+ * @param {number} count - Number to decide which translation to use (e.g. 2)
+ * @returns {string} Translated text with the number replaced (e.g. '2 days')
+ */
const ngettext = (text, pluralText, count) => {
const translated = locale
.ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count)
@@ -45,16 +45,16 @@ const ngettext = (text, pluralText, count) => {
};
/**
- Translate context based text
- Either pass in the context translation like `Context|Text to translate`
- or allow for dynamic text by doing passing in the context first & then the text to translate
-
- @param keyOrContext Can be either the key to translate including the context
- (eg. 'Context|Text') or just the context for the translation
- (eg. 'Context')
- @param key Is the dynamic variable you want to be translated
- @returns {String} Translated context based text
-*/
+ * Translate context based text.
+ * @example
+ * s__('Context|Text to translate');
+ * @example
+ * s__('Context', 'Text to translate');
+ * @param {string} keyOrContext - Context and a key to translation (e.g. 'Context|Text')
+ * or just a context (e.g. 'Context')
+ * @param {string} [key] - if `keyOrContext` is just a context, this is the key to translation
+ * @returns {string} Translated context based text
+ */
const pgettext = (keyOrContext, key) => {
const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext);
const translated = gettext(normalizedKey).split('|');
@@ -102,21 +102,19 @@ export const getPreferredLocales = () => {
};
/**
- Creates an instance of Intl.DateTimeFormat for the current locale.
-
- @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
- @returns {Intl.DateTimeFormat}
-*/
+ * Creates an instance of Intl.DateTimeFormat for the current locale.
+ * @param {Intl.DateTimeFormatOptions} [formatOptions] - for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+ * @returns {Intl.DateTimeFormat}
+ */
const createDateTimeFormat = (formatOptions) =>
Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
/**
* Formats a number as a string using `toLocaleString`.
- *
- * @param {Number} value - number to be converted
- * @param {options?} options - options to be passed to
- * `toLocaleString` such as `unit` and `style`.
- * @param {langCode?} langCode - If set, forces a different
+ * @param {number} value - number to be converted
+ * @param {Intl.NumberFormatOptions} [options] - options to be passed to
+ * `toLocaleString`.
+ * @param {string|string[]} [langCode] - If set, forces a different
* language code from the one currently in the document.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
*
diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js
index 12df67670f9..7faf9390684 100644
--- a/app/assets/javascripts/locale/sprintf.js
+++ b/app/assets/javascripts/locale/sprintf.js
@@ -1,17 +1,15 @@
import { escape } from 'lodash';
/**
- Very limited implementation of sprintf supporting only named parameters.
-
- @param input (translated) text with parameters (e.g. '%{num_users} users use us')
- @param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 })
- @param {Boolean} escapeParameters whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
- @returns {String} the text with parameters replaces (e.g. '5 users use us')
-
- @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
- @see https://gitlab.com/gitlab-org/gitlab-foss/issues/37992
-*/
-export default (input, parameters, escapeParameters = true) => {
+ * Very limited implementation of sprintf supporting only named parameters.
+ * @param {string} input - (translated) text with parameters (e.g. '%{num_users} users use us')
+ * @param {Object.<string, string|number>} [parameters] - object mapping parameter names to values (e.g. { num_users: 5 })
+ * @param {boolean} [escapeParameters=true] - whether parameter values should be escaped (see https://lodash.com/docs/4.17.15#escape)
+ * @returns {string} the text with parameters replaces (e.g. '5 users use us')
+ * @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
+ * @see https://gitlab.com/gitlab-org/gitlab-foss/issues/37992
+ */
+export default function sprintf(input, parameters, escapeParameters = true) {
let output = input;
output = output.replace(/%+/g, '%');
@@ -29,4 +27,4 @@ export default (input, parameters, escapeParameters = true) => {
}
return output;
-};
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index fd002e29afc..5bfdd174694 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -8,7 +8,6 @@ import './commons';
import './behaviors';
// lib/utils
-import applyGitLabUIConfig from '@gitlab/ui/dist/config';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
@@ -44,8 +43,6 @@ import 'jh_else_ce/main_jh';
logHelloDeferred();
-applyGitLabUIConfig();
-
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
window.$ = jQuery;
diff --git a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index 1ae341820d1..2f9acc96923 100644
--- a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
@@ -40,7 +40,7 @@ export default {
v-gl-tooltip.hover
:title="$options.title"
:aria-label="$options.title"
- data-qa-selector="approve_access_request_button"
+ data-testid="approve-access-request-button"
icon="check"
type="submit"
/>
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index caa292b37ce..1f134fe3f5d 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -71,7 +71,7 @@ export default {
:title="title"
:aria-label="title"
icon="remove"
- data-qa-selector="delete_member_button"
+ data-testid="delete-member-button"
@click="showRemoveMemberModal(modalData)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
index c7bd1525558..ecc769174f4 100644
--- a/app/assets/javascripts/members/components/modals/remove_member_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -56,7 +56,8 @@ export default {
actionText() {
if (this.isAccessRequest) {
return __('Deny access request');
- } else if (this.isInvite) {
+ }
+ if (this.isInvite) {
return s__('Member|Revoke invite');
}
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 407cbc55dd3..cac8c9fb4db 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -32,9 +32,11 @@ export default {
memberType() {
if (this.isGroup) {
return MEMBER_TYPES.group;
- } else if (this.isInvite) {
+ }
+ if (this.isInvite) {
return MEMBER_TYPES.invite;
- } else if (this.isAccessRequest) {
+ }
+ if (this.isAccessRequest) {
return MEMBER_TYPES.accessRequest;
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 09fe611262c..1bc67522e82 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -93,7 +93,11 @@ function mountPipelines() {
const { mrWidgetData } = gl;
const table = new Vue({
components: {
- CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
+ CommitPipelinesTable: () => {
+ return gon.features.mrPipelinesGraphql
+ ? import('~/ci/merge_requests/components/pipelines_table_wrapper.vue')
+ : import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue');
+ },
},
apolloProvider,
provide: {
@@ -103,6 +107,8 @@ function mountPipelines() {
fullPath: pipelineTableViewEl.dataset.fullPath,
graphqlPath: pipelineTableViewEl.dataset.graphqlPath,
manualActionsLimit: 50,
+ mergeRequestId: mrWidgetData ? mrWidgetData.iid : null,
+ sourceProjectFullPath: mrWidgetData?.source_project_full_path || '',
withFailedJobsDetails: true,
},
render(createElement) {
@@ -573,10 +579,12 @@ export default class MergeRequestTabs {
expandViewContainer() {
this.contentWrapper.classList.remove('container-limited');
+ this.contentWrapper.classList.add('diffs-container-limited');
}
resetViewContainer() {
this.contentWrapper.classList.toggle('container-limited', this.isFixedLayoutPreferred);
+ this.contentWrapper.classList.remove('diffs-container-limited');
}
// Expand the issuable sidebar unless the user explicitly collapsed it
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index 8e02048f494..c7c16e91e4c 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -23,9 +23,6 @@ export default {
currentProject: {
default: () => ({}),
},
- currentBranch: {
- default: () => ({}),
- },
inputs: {
default: () => ({}),
},
@@ -35,8 +32,12 @@ export default {
toggleClass: {
default: () => ({}),
},
- branchQaSelector: {
- default: '',
+ },
+ props: {
+ currentBranch: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
},
data() {
@@ -57,6 +58,12 @@ export default {
return this.commitHtml || this.loading || !this.selectedBranch.value;
},
},
+ watch: {
+ currentBranch(newVal) {
+ this.selectedBranch = newVal;
+ this.fetchCommit();
+ },
+ },
mounted() {
this.fetchCommit();
},
@@ -67,6 +74,7 @@ export default {
selectBranch(branch) {
this.selectedBranch = branch;
this.fetchCommit();
+ this.$emit('select-branch', branch.value);
},
async fetchCommit() {
if (!this.selectedBranch.value) return;
@@ -108,7 +116,7 @@ export default {
:input-name="inputs.branch.name"
:default="currentBranch"
:toggle-class="toggleClass.branch"
- :qa-selector="branchQaSelector"
+ data-testid="compare-dropdown"
@selected="selectBranch"
/>
</div>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index a5a4e683214..2855d704507 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -46,11 +46,6 @@ export default {
required: false,
default: '',
},
- qaSelector: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -70,6 +65,12 @@ export default {
);
},
},
+ watch: {
+ default(newVal) {
+ this.current = newVal;
+ this.selected = newVal.value;
+ },
+ },
methods: {
async fetchData() {
if (!this.endpoint) return;
@@ -136,7 +137,7 @@ export default {
'gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown',
toggleClass,
]"
- :data-qa-selector="qaSelector"
+ data-testid="source-branch-dropdown"
@shown="fetchData"
@search="searchData"
@select="selectItem"
diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/merge_requests/components/header_metadata.vue
index a0854be099d..fce7ba385b4 100644
--- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
+++ b/app/assets/javascripts/merge_requests/components/header_metadata.vue
@@ -2,16 +2,10 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
-import { sprintf, __ } from '~/locale';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-const noteableTypeText = {
- issue: __('issue'),
- merge_request: __('merge request'),
-};
-
export default {
TYPE_ISSUE,
WORKSPACE_PROJECT,
@@ -22,7 +16,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
inject: ['hidden'],
computed: {
...mapGetters(['getNoteableData']),
@@ -32,26 +25,19 @@ export default {
isConfidential() {
return this.getNoteableData.confidential;
},
- isMergeRequest() {
- return this.getNoteableData.targetType === TYPE_MERGE_REQUEST;
- },
warningIconsMeta() {
return [
{
iconName: 'lock',
visible: this.isLocked,
dataTestId: 'locked',
- tooltip: sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: noteableTypeText[this.getNoteableData.targetType],
- }),
+ tooltip: __('This merge request is locked. Only project members can comment.'),
},
{
iconName: 'spam',
visible: this.hidden,
dataTestId: 'hidden',
- tooltip: sprintf(__('This %{issuable} is hidden because its author has been banned'), {
- issuable: noteableTypeText[this.getNoteableData.targetType],
- }),
+ tooltip: __('This merge request is hidden because its author has been banned'),
},
];
},
@@ -63,9 +49,9 @@ export default {
<div class="gl-display-inline-block">
<confidentiality-badge
v-if="isConfidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-mr-3"
:issuable-type="$options.TYPE_ISSUE"
+ :workspace-type="$options.WORKSPACE_PROJECT"
/>
<template v-for="meta in warningIconsMeta">
<div
@@ -74,11 +60,7 @@ export default {
v-gl-tooltip.bottom
:data-testid="meta.dataTestId"
:title="meta.tooltip || null"
- :class="{
- 'gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center': isMergeRequest,
- 'gl-display-inline-block': !isMergeRequest,
- }"
- class="issuable-warning-icon"
+ class="issuable-warning-icon gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center"
>
<gl-icon :name="meta.iconName" class="icon" />
</div>
diff --git a/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue b/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
new file mode 100644
index 00000000000..3d5478757a8
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/components/merge_request_status_badge.vue
@@ -0,0 +1,74 @@
+<script>
+import Vue from 'vue';
+import { fetchPolicies } from '~/lib/graphql';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+
+export const badgeState = Vue.observable({
+ state: '',
+ updateStatus: null,
+});
+
+export default {
+ components: {
+ StatusBadge,
+ },
+ inject: {
+ query: { default: null },
+ projectPath: { default: null },
+ iid: { default: null },
+ },
+ props: {
+ initialState: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ issuableType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ if (!this.iid) {
+ return {
+ state: this.initialState,
+ };
+ }
+
+ if (!badgeState.state && this.initialState) {
+ badgeState.state = this.initialState;
+ }
+
+ return badgeState;
+ },
+ created() {
+ if (!badgeState.updateStatus) {
+ badgeState.updateStatus = this.fetchState;
+ }
+ },
+ beforeDestroy() {
+ if (badgeState.updateStatus && this.query) {
+ badgeState.updateStatus = null;
+ }
+ },
+ methods: {
+ async fetchState() {
+ const { data } = await this.$apollo.query({
+ query: this.query,
+ variables: {
+ projectPath: this.projectPath,
+ iid: this.iid,
+ },
+ fetchPolicy: fetchPolicies.NO_CACHE,
+ });
+
+ badgeState.state = data?.workspace?.issuable?.state;
+ },
+ },
+};
+</script>
+
+<template>
+ <status-badge class="gl-align-self-center gl-mr-3" :issuable-type="issuableType" :state="state" />
+</template>
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index 3c3bee9b108..c1e88a901c4 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -7,13 +7,15 @@ import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isLoggedIn } from '~/lib/utils/common_utils';
-import StatusBox from '~/issuable/components/status_box.vue';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
import TodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import titleSubscription from '../queries/title.subscription.graphql';
export default {
+ TYPE_MERGE_REQUEST,
apollo: {
$subscribe: {
title: {
@@ -41,8 +43,8 @@ export default {
GlLink,
GlSprintf,
GlBadge,
- StatusBox,
DiscussionCounter,
+ StatusBadge,
TodoWidget,
ClipboardButton,
},
@@ -115,7 +117,11 @@ export default {
:class="{ 'gl-max-w-container-xl': !isFluidLayout }"
>
<div class="gl-w-full gl-display-flex gl-align-items-baseline">
- <status-box :initial-state="getNoteableData.state" issuable-type="merge_request" />
+ <status-badge
+ class="gl-align-self-center gl-mr-3"
+ :issuable-type="$options.TYPE_MERGE_REQUEST"
+ :state="getNoteableData.state"
+ />
<a
v-safe-html:[$options.safeHtmlConfig]="titleHtml"
href="#top"
diff --git a/app/assets/javascripts/merge_requests/index.js b/app/assets/javascripts/merge_requests/index.js
new file mode 100644
index 00000000000..29218eb53e0
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import HeaderMetadata from './components/header_metadata.vue';
+
+export function mountHeaderMetadata(store) {
+ const el = document.querySelector('.js-header-metadata-root');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'HeaderMetadataRoot',
+ store,
+ provide: { hidden: parseBoolean(el.dataset.hidden) },
+ render: (createElement) => createElement(HeaderMetadata),
+ });
+}
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index c8268b1a9ae..a7b753b7ca8 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -33,19 +33,26 @@ export default {
outputType(output) {
if (output.text) {
return 'text/plain';
- } else if (output.output_type === ERROR_OUTPUT_TYPE) {
+ }
+ if (output.output_type === ERROR_OUTPUT_TYPE) {
return 'error';
- } else if (output.data['image/png']) {
+ }
+ if (output.data['image/png']) {
return 'image/png';
- } else if (output.data['image/jpeg']) {
+ }
+ if (output.data['image/jpeg']) {
return 'image/jpeg';
- } else if (output.data['text/html']) {
+ }
+ if (output.data['text/html']) {
return 'text/html';
- } else if (output.data['text/latex']) {
+ }
+ if (output.data['text/latex']) {
return 'text/latex';
- } else if (output.data['image/svg+xml']) {
+ }
+ if (output.data['image/svg+xml']) {
return 'image/svg+xml';
- } else if (output.data[TEXT_MARKDOWN]) {
+ }
+ if (output.data[TEXT_MARKDOWN]) {
return TEXT_MARKDOWN;
}
@@ -63,21 +70,29 @@ export default {
getComponent(output) {
if (output.text) {
return CodeOutput;
- } else if (output.output_type === ERROR_OUTPUT_TYPE) {
+ }
+ if (output.output_type === ERROR_OUTPUT_TYPE) {
return ErrorOutput;
- } else if (output.data['image/png']) {
+ }
+ if (output.data['image/png']) {
return ImageOutput;
- } else if (output.data['image/jpeg']) {
+ }
+ if (output.data['image/jpeg']) {
return ImageOutput;
- } else if (isDataframe(output)) {
+ }
+ if (isDataframe(output)) {
return DataframeOutput;
- } else if (output.data['text/html']) {
+ }
+ if (output.data['text/html']) {
return HtmlOutput;
- } else if (output.data['text/latex']) {
+ }
+ if (output.data['text/latex']) {
return LatexOutput;
- } else if (output.data['image/svg+xml']) {
+ }
+ if (output.data['image/svg+xml']) {
return HtmlOutput;
- } else if (output.data[TEXT_MARKDOWN]) {
+ }
+ if (output.data[TEXT_MARKDOWN]) {
return MarkdownOutput;
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index a009f2975bb..144cfa4295b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -5,7 +5,6 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
-import { badgeState } from '~/issuable/components/status_box.vue';
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants';
import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
import {
@@ -14,6 +13,7 @@ import {
slugifyWithUnderscore,
} from '~/lib/utils/text_utility';
import { sprintf } from '~/locale';
+import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -210,8 +210,6 @@ export default {
methods: {
...mapActions([
'saveNote',
- 'stopPolling',
- 'restartPolling',
'removePlaceholderNotes',
'closeIssuable',
'reopenIssuable',
@@ -253,7 +251,6 @@ export default {
}
this.note = ''; // Empty textarea while being requested. Repopulate in catch
- this.stopPolling();
this.isSubmitting = true;
@@ -264,7 +261,6 @@ export default {
this.saveNote(noteData)
.then(() => {
- this.restartPolling();
this.discard();
if (withIssueAction) {
@@ -381,7 +377,10 @@ export default {
@input="onInput"
/>
</comment-field-layout>
- <div class="note-form-actions">
+ <div
+ class="note-form-actions gl-font-size-0"
+ :class="{ 'gl-display-flex gl-gap-3': hasDrafts }"
+ >
<template v-if="hasDrafts">
<gl-button
:disabled="disableSubmitButton"
@@ -404,7 +403,7 @@ export default {
<gl-form-checkbox
v-if="canSetInternalNote"
v-model="noteIsInternal"
- class="gl-mb-2"
+ class="gl-mb-2 gl-flex-basis-full"
data-testid="internal-note-checkbox"
>
{{ $options.i18n.internal }}
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 7266cdb6405..90f7a6862f0 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -137,7 +137,8 @@ export default {
filterType(value) {
if (value === 0) {
return DISCUSSION_FILTER_TYPES.ALL;
- } else if (value === 1) {
+ }
+ if (value === 1) {
return DISCUSSION_FILTER_TYPES.COMMENTS;
}
return DISCUSSION_FILTER_TYPES.HISTORY;
diff --git a/app/assets/javascripts/notes/components/mr_discussion_filter.vue b/app/assets/javascripts/notes/components/mr_discussion_filter.vue
index 08d3670ae6a..c2ac95ca56e 100644
--- a/app/assets/javascripts/notes/components/mr_discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/mr_discussion_filter.vue
@@ -36,7 +36,8 @@ export default {
if (length === MR_FILTER_OPTIONS.length) {
return __('All activity');
- } else if (length > 1) {
+ }
+ if (length > 1) {
return `%{strongStart}${firstSelected.text}%{strongEnd} +${length - 1} more`;
}
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 8b43f068f11..363383fd7ad 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -160,12 +160,14 @@ export default {
filePath: this.diffFile.file_path,
refs: this.diffFile.diff_refs,
};
- } else if (this.note && this.note.position) {
+ }
+ if (this.note && this.note.position) {
return {
filePath: this.note.position.new_path,
refs: this.note.position,
};
- } else if (this.discussion && this.discussion.diff_file) {
+ }
+ if (this.discussion && this.discussion.diff_file) {
return {
filePath: this.discussion.diff_file.file_path,
refs: this.discussion.diff_file.diff_refs,
@@ -381,8 +383,8 @@ export default {
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
/>
</comment-field-layout>
- <div class="note-form-actions">
- <p v-if="showResolveDiscussionToggle">
+ <div class="note-form-actions gl-font-size-0">
+ <template v-if="showResolveDiscussionToggle">
<label>
<template v-if="discussionResolved">
<gl-form-checkbox v-model="isUnresolving" class="js-unresolve-checkbox">
@@ -395,7 +397,7 @@ export default {
</gl-form-checkbox>
</template>
</label>
- </p>
+ </template>
<template v-if="showBatchCommentsActions">
<div class="gl-display-flex gl-flex-wrap gl-mb-n3">
@@ -432,7 +434,7 @@ export default {
</div>
</template>
<template v-else>
- <div class="gl-display-sm-flex gl-flex-wrap">
+ <div class="gl-display-sm-flex gl-flex-wrap gl-font-size-0">
<gl-button
:disabled="isDisabled"
category="primary"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 6fb958e810b..2524b9efdb6 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -169,7 +169,6 @@ export default {
});
},
beforeDestroy() {
- this.stopPolling();
window.removeEventListener('hashchange', this.handleHashChanged);
eventHub.$off('notesApp.updateIssuableConfidentiality', this.setConfidentiality);
},
@@ -182,7 +181,6 @@ export default {
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
- 'stopPolling',
'setConfidentiality',
'fetchNotes',
]),
diff --git a/app/assets/javascripts/notes/components/sidebar_subscription.vue b/app/assets/javascripts/notes/components/sidebar_subscription.vue
index f60a17eb36b..c02c7a57dfa 100644
--- a/app/assets/javascripts/notes/components/sidebar_subscription.vue
+++ b/app/assets/javascripts/notes/components/sidebar_subscription.vue
@@ -3,7 +3,7 @@
import { mapActions } from 'vuex';
import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import { fetchPolicies } from '~/lib/graphql';
-import { confidentialityQueries } from '~/sidebar/constants';
+import { confidentialityQueries } from '~/sidebar/queries/constants';
import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
export default {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 0444eca9aa7..7eb01897296 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,5 +1,4 @@
import $ from 'jquery';
-import Visibility from 'visibilityjs';
import Vue from 'vue';
import actionCable from '~/actioncable_consumer';
import Api from '~/api';
@@ -14,8 +13,6 @@ import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutatio
import updateMergeRequestLockMutation from '~/sidebar/queries/update_merge_request_lock.mutation.graphql';
import loadAwardsHandler from '~/awards_handler';
import { isInViewport, scrollToElement, isInMRPage } from '~/lib/utils/common_utils';
-import Poll from '~/lib/utils/poll';
-import { create } from '~/lib/utils/recurrence';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import sidebarTimeTrackingEventHub from '~/sidebar/event_hub';
import TaskList from '~/task_list';
@@ -30,9 +27,6 @@ import * as constants from '../constants';
import * as types from './mutation_types';
import * as utils from './utils';
-const NOTES_POLLING_INTERVAL = 6000;
-let eTagPoll;
-
export const updateLockedAttribute = ({ commit, getters }, { locked, fullPath }) => {
const { iid, targetType } = getters.getNoteableData;
@@ -152,29 +146,25 @@ export const initPolling = ({ state, dispatch, getters, commit }) => {
dispatch('setLastFetchedAt', getters.getNotesDataByProp('lastFetchedAt'));
- if (gon.features?.actionCableNotes) {
- actionCable.subscriptions.create(
- {
- channel: 'Noteable::NotesChannel',
- project_id: state.notesData.projectId,
- group_id: state.notesData.groupId,
- noteable_type: state.notesData.noteableType,
- noteable_id: state.notesData.noteableId,
+ actionCable.subscriptions.create(
+ {
+ channel: 'Noteable::NotesChannel',
+ project_id: state.notesData.projectId,
+ group_id: state.notesData.groupId,
+ noteable_type: state.notesData.noteableType,
+ noteable_id: state.notesData.noteableId,
+ },
+ {
+ connected() {
+ dispatch('fetchUpdatedNotes');
},
- {
- connected() {
+ received(data) {
+ if (data.event === 'updated') {
dispatch('fetchUpdatedNotes');
- },
- received(data) {
- if (data.event === 'updated') {
- dispatch('fetchUpdatedNotes');
- }
- },
+ }
},
- );
- } else {
- dispatch('poll');
- }
+ },
+ );
commit(types.SET_IS_POLLING_INITIALIZED, true);
};
@@ -386,7 +376,8 @@ export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }
if (!discussion) {
return Promise.reject();
- } else if (isResolved) {
+ }
+ if (isResolved) {
return Promise.resolve();
}
@@ -515,8 +506,6 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
{"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}}
*/
if (hasQuickActions && message) {
- if (eTagPoll) eTagPoll.makeRequest();
-
// synchronizing the quick action with the sidebar widget
// this is a temporary solution until we have confidentiality real-time updates
if (
@@ -624,69 +613,7 @@ export const fetchUpdatedNotes = ({ commit, state, getters, dispatch }) => {
.then(({ data }) => {
pollSuccessCallBack(data, commit, state, getters, dispatch);
})
- .catch(() => {
- createAlert({
- message: __('Something went wrong while fetching latest comments.'),
- });
- });
-};
-
-export const poll = ({ commit, state, getters, dispatch }) => {
- const notePollOccurrenceTracking = create();
- let alert;
-
- notePollOccurrenceTracking.handle(1, () => {
- // Since polling halts internally after 1 failure, we manually try one more time
- setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
- });
- notePollOccurrenceTracking.handle(2, () => {
- // On the second failure in a row, show the alert and try one more time (hoping to succeed and clear the error)
- alert = createAlert({
- message: __('Something went wrong while fetching latest comments.'),
- });
- setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
- });
-
- eTagPoll = new Poll({
- resource: {
- poll: () => {
- const { endpoint, options } = getFetchDataParams(state);
- return axios.get(endpoint, options);
- },
- },
- method: 'poll',
- successCallback: ({ data }) => {
- pollSuccessCallBack(data, commit, state, getters, dispatch);
-
- if (notePollOccurrenceTracking.count) {
- notePollOccurrenceTracking.reset();
- }
- alert?.dismiss();
- },
- errorCallback: () => notePollOccurrenceTracking.occur(),
- });
-
- if (!Visibility.hidden()) {
- eTagPoll.makeDelayedRequest(2500);
- } else {
- eTagPoll.makeRequest();
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- eTagPoll.restart();
- } else {
- eTagPoll.stop();
- }
- });
-};
-
-export const stopPolling = () => {
- if (eTagPoll) eTagPoll.stop();
-};
-
-export const restartPolling = () => {
- if (eTagPoll) eTagPoll.restart();
+ .catch(() => {});
};
export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
@@ -766,7 +693,6 @@ export const submitSuggestion = (
dispatch('resolveDiscussion', { discussionId }).catch(() => {});
commit(types.SET_RESOLVING_DISCUSSION, true);
- dispatch('stopPolling');
return Api.applySuggestion(suggestionId, message)
.then(dispatchResolveDiscussion)
@@ -786,7 +712,6 @@ export const submitSuggestion = (
})
.finally(() => {
commit(types.SET_RESOLVING_DISCUSSION, false);
- dispatch('restartPolling');
});
};
@@ -801,7 +726,6 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
commit(types.SET_APPLYING_BATCH_STATE, true);
commit(types.SET_RESOLVING_DISCUSSION, true);
- dispatch('stopPolling');
return Api.applySuggestionBatch(suggestionIds, message)
.then(() => Promise.all(resolveAllDiscussions()))
@@ -823,7 +747,6 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
.finally(() => {
commit(types.SET_APPLYING_BATCH_STATE, false);
commit(types.SET_RESOLVING_DISCUSSION, false);
- dispatch('restartPolling');
});
};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 3fb9913bdcb..c43430639ad 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,8 +1,8 @@
import { flattenDeep, clone } from 'lodash';
import { match } from '~/diffs/utils/diff_file';
-import { badgeState } from '~/issuable/components/status_box.vue';
import { isInMRPage } from '~/lib/utils/common_utils';
import { doesHashExistInUrl } from '~/lib/utils/url_utility';
+import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
import * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index c55600f3db2..718001e98fe 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,57 +1,62 @@
+import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
-// import mockData from './mock_traces.json';
-
-function enableTraces() {
- // TODO remove mocks https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2271
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve();
- }, 1000);
- });
-}
-function isTracingEnabled() {
- // TODO remove mocks https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2271
- return new Promise((resolve) => {
- setTimeout(() => {
- // Currently relying on manual provisioning, hence assuming tracing is enabled
- resolve(true);
- }, 1000);
- });
+function reportErrorAndThrow(e) {
+ Sentry.captureException(e);
+ throw e;
}
-
-function traceWithDuration(trace) {
- // aggregating duration on the client for now, but expecting to be coming from the backend
- // https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2274
- const duration = trace.spans[0].duration_nano;
- return {
- ...trace,
- duration: duration / 1000,
- };
+// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L59
+async function enableTraces(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, {
+ method: 'put',
+ withCredentials: true,
+ });
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
}
-async function fetchTrace(tracingUrl, traceId) {
- if (!traceId) {
- throw new Error('traceId is required.');
+// Provisioning API spec: https://gitlab.com/gitlab-org/opstrace/opstrace/-/blob/main/provisioning-api/pkg/provisioningapi/routes.go#L37
+async function isTracingEnabled(provisioningUrl) {
+ try {
+ const { data } = await axios.get(provisioningUrl, { withCredentials: true });
+ if (data && data.status) {
+ // we currently ignore the 'status' payload and just check if the request was successful
+ // We might improve this as part of https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2315
+ return true;
+ }
+ } catch (e) {
+ if (e.response.status === 404) {
+ return false;
+ }
+ return reportErrorAndThrow(e);
}
+ return reportErrorAndThrow(new Error('Failed to check provisioning')); // eslint-disable-line @gitlab/require-i18n-strings
+}
- const { data } = await axios.get(tracingUrl, {
- withCredentials: true,
- params: {
- trace_id: traceId,
- },
- });
+async function fetchTrace(tracingUrl, traceId) {
+ try {
+ if (!traceId) {
+ throw new Error('traceId is required.');
+ }
+
+ const { data } = await axios.get(tracingUrl, {
+ withCredentials: true,
+ params: {
+ trace_id: traceId,
+ },
+ });
- // TODO: Improve local GDK dev experience with tracing https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2308
- // const data = mockData;
- // const trace = data.traces.find((t) => t.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
+ }
- 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];
+ } catch (e) {
+ return reportErrorAndThrow(e);
}
-
- const trace = data.traces[0];
- return traceWithDuration(trace);
}
/**
@@ -164,18 +169,18 @@ function filterObjToQueryParams(filterObj) {
async function fetchTraces(tracingUrl, filters = {}) {
const filterParams = filterObjToQueryParams(filters);
- const { data } = await axios.get(tracingUrl, {
- withCredentials: true,
- params: filterParams,
- });
- // TODO: Improve local GDK dev experience with tracing https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2308
- // Uncomment the line below to test this locally
- // const data = mockData;
-
- if (!Array.isArray(data.traces)) {
- throw new Error('traces are missing/invalid in the response.'); // eslint-disable-line @gitlab/require-i18n-strings
+ try {
+ const { data } = await axios.get(tracingUrl, {
+ withCredentials: true,
+ params: filterParams,
+ });
+ if (!Array.isArray(data.traces)) {
+ throw new Error('traces are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+ return data.traces;
+ } catch (e) {
+ return reportErrorAndThrow(e);
}
- return data.traces.map(traceWithDuration);
}
export function buildClient({ provisioningUrl, tracingUrl }) {
diff --git a/app/assets/javascripts/observability/mock_traces.json b/app/assets/javascripts/observability/mock_traces.json
index ee59258e591..cd7dfb40af6 100644
--- a/app/assets/javascripts/observability/mock_traces.json
+++ b/app/assets/javascripts/observability/mock_traces.json
@@ -1,348 +1,107 @@
{
- "project_id": "10141740",
+ "project_id": 123,
"traces": [
{
- "timestamp": "2023-07-18T10:31:23.661285Z",
- "trace_id": "08a1b018-e1b9-88b2-094b-ca5fd40783ad",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:23.661285Z",
- "span_id": "30A9220B254C42B1",
- "trace_id": "08a1b018-e1b9-88b2-094b-ca5fd40783ad",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 250,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:17.026724Z",
- "trace_id": "1c2099e0-6da8-d5fb-a91d-bdd5a5bea82c",
- "service_name": "my-service-name2",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:17.026724Z",
- "span_id": "154925D3DA2C1307",
- "trace_id": "1c2099e0-6da8-d5fb-a91d-bdd5a5bea82c",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 208,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:21.602132Z",
- "trace_id": "f4c2f964-afee-cc2e-bd1a-c654ff55db4e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:21.602132Z",
- "span_id": "53A4AE94DFF72A28",
- "trace_id": "f4c2f964-afee-cc2e-bd1a-c654ff55db4e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 5125,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:14.772009Z",
- "trace_id": "fa6302ad-7214-7c05-40f7-91195356774e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:14.772009Z",
- "span_id": "5BB240D099656820",
- "trace_id": "fa6302ad-7214-7c05-40f7-91195356774e",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 1584,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:22.623552Z",
- "trace_id": "54021e57-a25c-c6fe-1f53-542bbdbcb16c",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:22.623552Z",
- "span_id": "C5AE65D0C26BF3FD",
- "trace_id": "54021e57-a25c-c6fe-1f53-542bbdbcb16c",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 750,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:21.602156Z",
- "trace_id": "34d455cc-e518-fb4e-513f-e88030d4ccc8",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:21.602156Z",
- "span_id": "5288B61252594EB2",
- "trace_id": "34d455cc-e518-fb4e-513f-e88030d4ccc8",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 750,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:20.567364Z",
- "trace_id": "3892a93a-f4eb-b416-372e-3c9237be97e3",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:20.567364Z",
- "span_id": "1D690E5094345C98",
- "trace_id": "3892a93a-f4eb-b416-372e-3c9237be97e3",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 958,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:23.661289Z",
- "trace_id": "9d0630d5-21b5-686f-57cb-d97c647fc31f",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:23.661289Z",
- "span_id": "8F548EE08F9C2EAC",
- "trace_id": "9d0630d5-21b5-686f-57cb-d97c647fc31f",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 167,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:14.77197Z",
- "trace_id": "f2470b0e-3bbb-8af2-68f8-c97343fba7ee",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:14.77197Z",
- "span_id": "6B5AB710CE8A4471",
- "trace_id": "f2470b0e-3bbb-8af2-68f8-c97343fba7ee",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 5583,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:17.026712Z",
- "trace_id": "f4e64ee0-ee32-0edb-c4d1-a15f4047bdc4",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:17.026712Z",
- "span_id": "199D402DE1A29F3F",
- "trace_id": "f4e64ee0-ee32-0edb-c4d1-a15f4047bdc4",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 6959,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:20.567337Z",
- "trace_id": "344b4db1-c890-514c-b94f-425fff3a795b",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:20.567337Z",
- "span_id": "CAC38748150E5A0C",
- "trace_id": "344b4db1-c890-514c-b94f-425fff3a795b",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 3917,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:22.623559Z",
- "trace_id": "40b933be-11d7-7b41-e63a-ff7e7c5d50ab",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:22.623559Z",
- "span_id": "3485100A27958F59",
- "trace_id": "40b933be-11d7-7b41-e63a-ff7e7c5d50ab",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 709,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:17.026723Z",
- "trace_id": "c3347c43-316f-2b08-0b46-7d142d6764b7",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:17.026723Z",
- "span_id": "1CF28C36AB7EB3F9",
- "trace_id": "c3347c43-316f-2b08-0b46-7d142d6764b7",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 208,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:23.661272Z",
- "trace_id": "762f9104-d3db-c762-d6d7-0476ca21249f",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:23.661272Z",
- "span_id": "83D8D6D2BD99A4D1",
- "trace_id": "762f9104-d3db-c762-d6d7-0476ca21249f",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 10000,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:22.623524Z",
- "trace_id": "b46ded15-f900-fba7-7396-a6b453221038",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:22.623524Z",
- "span_id": "EB84455AE35DEAD5",
- "trace_id": "b46ded15-f900-fba7-7396-a6b453221038",
- "service_name": "my-service-name",
- "operation": "Multiplication",
- "duration_nano": 17666,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:21.60216Z",
- "trace_id": "8dcd7b90-7f94-6b19-4a8f-b681801568ba",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:21.60216Z",
- "span_id": "A5C773414186949D",
- "trace_id": "8dcd7b90-7f94-6b19-4a8f-b681801568ba",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 250,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:14.772014Z",
- "trace_id": "51e23c12-033d-a0db-d2b0-2f3f4e3d9fb3",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:14.772014Z",
- "span_id": "3397060046FD4428",
- "trace_id": "51e23c12-033d-a0db-d2b0-2f3f4e3d9fb3",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 291,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
- },
- {
- "timestamp": "2023-07-18T10:31:20.567369Z",
- "trace_id": "d54b5015-3938-ddf8-3aa1-49882503233e",
- "service_name": "my-service-name",
- "operation": "Addition",
- "statusCode": "STATUS_CODE_UNSET",
- "spans": [
- {
- "timestamp": "2023-07-18T10:31:20.567369Z",
- "span_id": "DAC36ACC2DBA8B11",
- "trace_id": "d54b5015-3938-ddf8-3aa1-49882503233e",
- "service_name": "my-service-name",
- "operation": "Addition",
- "duration_nano": 208,
- "statusCode": "STATUS_CODE_UNSET"
- }
- ],
- "totalSpans": 1
+ "timestamp": "2023-08-07T15:03:32.199806Z",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "tracegentracegentracegenttracegentracegentracegent",
+ "operation": "lets-golets-golets-goletslets-golets-golets-golets",
+ "statusCode": "STATUS_CODE_UNSET",
+ "duration_nano": 100120000,
+ "spans": [
+ {
+ "timestamp": "2023-08-07T15:03:32.199806Z",
+ "span_id": "A1FB81EB031B09E8",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "tracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegentracegen",
+ "operation": "lets-golets-golets-golets-golets-golets-golets-golets-golets-golets-golets-golets-go",
+ "duration_nano": 100120000,
+ "parent_span_id": "",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:03:32.199871Z",
+ "span_id": "9C920500FE9C85E3",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "tracegen",
+ "operation": "okey-dokey",
+ "duration_nano": 100055000,
+ "parent_span_id": "A1FB81EB031B09E8",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:03:53.199871Z",
+ "span_id": "FAKE",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "tracegen",
+ "operation": "okey-dokey",
+ "duration_nano": 50027500,
+ "parent_span_id": "9C920500FE9C85E3",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:03:53.199871Z",
+ "span_id": "FAKE-2",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "fake-service-2",
+ "operation": "okey-dokey",
+ "duration_nano": 50027500,
+ "parent_span_id": "9C920500FE9C85E3",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:04:13.199871Z",
+ "span_id": "FAKE-3",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "fake-service-3",
+ "operation": "okey-dokey",
+ "duration_nano": 30000000,
+ "parent_span_id": "FAKE-2",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:04:13.199871Z",
+ "span_id": "FAKE-4",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "fake-service-4",
+ "operation": "okey-dokey",
+ "duration_nano": 25000000,
+ "parent_span_id": "FAKE-3",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:04:13.199871Z",
+ "span_id": "FAKE-5",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "fake-service-5",
+ "operation": "okey-dokey",
+ "duration_nano": 10000000,
+ "parent_span_id": "FAKE-4",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:04:13.199871Z",
+ "span_id": "FAKE-6",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "fake-service-6",
+ "operation": "okey-dokey",
+ "duration_nano": 10000000,
+ "parent_span_id": "FAKE-5",
+ "statusCode": "STATUS_CODE_UNSET"
+ },
+ {
+ "timestamp": "2023-08-07T15:04:13.199871Z",
+ "span_id": "FAKE-7",
+ "trace_id": "dabb7ae1-2501-8e57-18e1-30ab21a9ab19",
+ "service_name": "fake-service-7",
+ "operation": "okey-dokey",
+ "duration_nano": 5000000,
+ "parent_span_id": "FAKE-6",
+ "statusCode": "STATUS_CODE_UNSET"
+ }
+ ],
+ "totalSpans": 5
}
],
- "totalTraces": 18
+ "totalTraces": 50
}
diff --git a/app/assets/javascripts/organizations/constants.js b/app/assets/javascripts/organizations/constants.js
new file mode 100644
index 00000000000..8ade37b169e
--- /dev/null
+++ b/app/assets/javascripts/organizations/constants.js
@@ -0,0 +1,4 @@
+export const RESOURCE_TYPE_GROUPS = 'groups';
+export const RESOURCE_TYPE_PROJECTS = 'projects';
+
+export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index 10471cc1fdd..dba738de5e1 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -13,9 +13,10 @@ import {
FILTERED_SEARCH_TERM,
TOKEN_EMPTY_SEARCH_TERM,
} from '~/vue_shared/components/filtered_search_bar/constants';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import GroupsView from '../../shared/components/groups_view.vue';
+import ProjectsView from '../../shared/components/projects_view.vue';
import {
- DISPLAY_QUERY_GROUPS,
- DISPLAY_QUERY_PROJECTS,
DISPLAY_LISTBOX_ITEMS,
SORT_DIRECTION_ASC,
SORT_DIRECTION_DESC,
@@ -23,8 +24,6 @@ import {
SORT_ITEM_CREATED,
FILTERED_SEARCH_TERM_KEY,
} from '../constants';
-import GroupsPage from './groups_page.vue';
-import ProjectsPage from './projects_page.vue';
export default {
i18n: {
@@ -45,14 +44,14 @@ export default {
const { display } = this.$route.query;
switch (display) {
- case DISPLAY_QUERY_GROUPS:
- return GroupsPage;
+ case RESOURCE_TYPE_GROUPS:
+ return GroupsView;
- case DISPLAY_QUERY_PROJECTS:
- return ProjectsPage;
+ case RESOURCE_TYPE_PROJECTS:
+ return ProjectsView;
default:
- return GroupsPage;
+ return GroupsView;
}
},
activeSortItem() {
@@ -80,9 +79,9 @@ export default {
displayListboxSelected() {
const { display } = this.$route.query;
- return [DISPLAY_QUERY_GROUPS, DISPLAY_QUERY_PROJECTS].includes(display)
+ return [RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS].includes(display)
? display
- : DISPLAY_QUERY_GROUPS;
+ : RESOURCE_TYPE_GROUPS;
},
},
methods: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
deleted file mode 100644
index 20db38403f7..00000000000
--- a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { s__ } from '~/locale';
-import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
-import groupsQuery from '../graphql/queries/groups.query.graphql';
-import { formatGroups } from '../utils';
-
-export default {
- i18n: {
- errorMessage: s__(
- 'Organization|An error occurred loading the groups. Please refresh the page to try again.',
- ),
- },
- components: { GlLoadingIcon, GroupsList },
- data() {
- return {
- groups: [],
- };
- },
- apollo: {
- groups: {
- query: groupsQuery,
- update(data) {
- return formatGroups(data.organization.groups.nodes);
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.groups.loading;
- },
- },
-};
-</script>
-
-<template>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <groups-list v-else :groups="groups" show-group-icon />
-</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
deleted file mode 100644
index d6958ee996e..00000000000
--- a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue
+++ /dev/null
@@ -1,46 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
-import { createAlert } from '~/alert';
-import projectsQuery from '../graphql/queries/projects.query.graphql';
-import { formatProjects } from '../utils';
-
-export default {
- i18n: {
- errorMessage: s__(
- 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
- ),
- },
- components: {
- ProjectsList,
- GlLoadingIcon,
- },
- data() {
- return {
- projects: [],
- };
- },
- apollo: {
- projects: {
- query: projectsQuery,
- update(data) {
- return formatProjects(data.organization.projects.nodes);
- },
- error(error) {
- createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
- },
- },
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.projects.loading;
- },
- },
-};
-</script>
-
-<template>
- <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list v-else :projects="projects" show-project-icon />
-</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/constants.js b/app/assets/javascripts/organizations/groups_and_projects/constants.js
index 529caa666a0..d79b632f6fb 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/constants.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/constants.js
@@ -3,8 +3,6 @@ import { __ } from '~/locale';
export const DISPLAY_QUERY_GROUPS = 'groups';
export const DISPLAY_QUERY_PROJECTS = 'projects';
-export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
-
export const FILTERED_SEARCH_TERM_KEY = 'search';
export const DISPLAY_LISTBOX_ITEMS = [
diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js
index f3f15c635f1..3e05e4d0a4c 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/index.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/index.js
@@ -2,9 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
-import resolvers from './graphql/resolvers';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
+import resolvers from '../shared/graphql/resolvers';
import App from './components/app.vue';
-import { ORGANIZATION_ROOT_ROUTE_NAME } from './constants';
export const createRouter = () => {
const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
@@ -23,6 +24,16 @@ export const initOrganizationsGroupsAndProjects = () => {
if (!el) return false;
+ const {
+ dataset: { appData },
+ } = el;
+ const {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
Vue.use(VueRouter);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers),
@@ -34,6 +45,12 @@ export const initOrganizationsGroupsAndProjects = () => {
name: 'OrganizationsGroupsAndProjects',
apolloProvider,
router,
+ provide: {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ },
render(createElement) {
return createElement(App);
},
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
new file mode 100644
index 00000000000..17ab7bd1d34
--- /dev/null
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -0,0 +1,258 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+// This is temporary mock data that will be removed when completing the following:
+// https://gitlab.com/gitlab-org/gitlab/-/issues/420777
+// https://gitlab.com/gitlab-org/gitlab/-/issues/421441
+
+export const organization = {
+ id: 'gid://gitlab/Organization/1',
+ __typename: 'Organization',
+};
+
+export const organizationProjects = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Project/8',
+ nameWithNamespace: 'Twitter / Typeahead.Js',
+ webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
+ topics: ['JavaScript', 'Vue.js'],
+ forksCount: 4,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'public',
+ openIssuesCount: 48,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: true,
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/7',
+ nameWithNamespace: 'Flightjs / Flight',
+ webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 37,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/6',
+ nameWithNamespace: 'Jashkenas / Underscore',
+ webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/5',
+ nameWithNamespace: 'Commit451 / Lab Coat',
+ webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 49,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/1',
+ nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
+ webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ isForked: false,
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ ],
+};
+
+export const organizationGroups = {
+ nodes: [
+ {
+ id: 'gid://gitlab/Group/29',
+ fullName: 'Commit451',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/Commit451',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 0,
+ projectsCount: 3,
+ groupMembersCount: 2,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/33',
+ fullName: 'Flightjs',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/flightjs',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:60" dir="auto">Ipsa reiciendis deleniti officiis illum nostrum quo aliquam.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 3,
+ groupMembersCount: 1,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/24',
+ fullName: 'Gitlab Org',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gitlab-org',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:64" dir="auto">Dolorem dolorem omnis impedit cupiditate pariatur officia velit.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 2,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/27',
+ fullName: 'Gnuwget',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/gnuwgetf',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:47" dir="auto">Culpa soluta aut eius dolores est vel sapiente.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 2,
+ groupMembersCount: 3,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/31',
+ fullName: 'Jashkenas',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/jashkenas',
+ descriptionHtml: '<p data-sourcepos="1:1-1:25" dir="auto">Ut ut id aliquid nostrum.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 3,
+ projectsCount: 3,
+ groupMembersCount: 10,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/22',
+ fullName: 'Toolbox',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/toolbox',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:46" dir="auto">Quo voluptatem magnam facere voluptates alias.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 2,
+ projectsCount: 3,
+ groupMembersCount: 40,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/35',
+ fullName: 'Twitter',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/twitter',
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Quae nulla consequatur assumenda id quo.</p>',
+ avatarUrl: null,
+ descendantGroupsCount: 20,
+ projectsCount: 30,
+ groupMembersCount: 100,
+ visibility: 'public',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/73',
+ fullName: 'test',
+ parent: null,
+ webUrl: 'http://127.0.0.1:3000/groups/test',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 1,
+ projectsCount: 1,
+ groupMembersCount: 1,
+ visibility: 'private',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Group/74',
+ fullName: 'Twitter / test subgroup',
+ parent: {
+ id: 'gid://gitlab/Group/35',
+ },
+ webUrl: 'http://127.0.0.1:3000/groups/twitter/test-subgroup',
+ descriptionHtml: '',
+ avatarUrl: null,
+ descendantGroupsCount: 4,
+ projectsCount: 4,
+ groupMembersCount: 4,
+ visibility: 'internal',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ ],
+};
diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue
new file mode 100644
index 00000000000..eaa3017ef97
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { s__, __ } from '~/locale';
+import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue';
+import groupsQuery from '../graphql/queries/groups.query.graphql';
+import { formatGroups } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the groups. Please refresh the page to try again.',
+ ),
+ emptyState: {
+ title: s__("Organization|You don't have any groups yet."),
+ description: s__(
+ 'Organization|A group is a collection of several projects. If you organize your projects under a group, it works like a folder.',
+ ),
+ primaryButtonText: __('New group'),
+ },
+ },
+ components: { GlLoadingIcon, GlEmptyState, GroupsList },
+ inject: {
+ groupsEmptyStateSvgPath: {},
+ newGroupPath: {
+ default: null,
+ },
+ },
+ props: {
+ shouldShowEmptyStateButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ groups: [],
+ };
+ },
+ apollo: {
+ groups: {
+ query: groupsQuery,
+ update(data) {
+ return formatGroups(data.organization.groups.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.groups.loading;
+ },
+ emptyStateProps() {
+ const baseProps = {
+ svgHeight: 144,
+ svgPath: this.groupsEmptyStateSvgPath,
+ title: this.$options.i18n.emptyState.title,
+ description: this.$options.i18n.emptyState.description,
+ };
+
+ if (this.shouldShowEmptyStateButtons && this.newGroupPath) {
+ return {
+ ...baseProps,
+ primaryButtonLink: this.newGroupPath,
+ primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
+ };
+ }
+
+ return baseProps;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <groups-list v-else-if="groups.length" :groups="groups" show-group-icon />
+ <gl-empty-state v-else v-bind="emptyStateProps" />
+</template>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
new file mode 100644
index 00000000000..9bf4e597884
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { createAlert } from '~/alert';
+import projectsQuery from '../graphql/queries/projects.query.graphql';
+import { formatProjects } from '../utils';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ 'Organization|An error occurred loading the projects. Please refresh the page to try again.',
+ ),
+ emptyState: {
+ title: s__("Organization|You don't have any projects yet."),
+ description: s__(
+ 'GroupsEmptyState|Projects are where you can store your code, access issues, wiki, and other features of Gitlab.',
+ ),
+ primaryButtonText: __('New project'),
+ },
+ },
+ components: {
+ ProjectsList,
+ GlLoadingIcon,
+ GlEmptyState,
+ },
+ inject: {
+ projectsEmptyStateSvgPath: {},
+ newProjectPath: {
+ default: null,
+ },
+ },
+ props: {
+ shouldShowEmptyStateButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ projects: [],
+ };
+ },
+ apollo: {
+ projects: {
+ query: projectsQuery,
+ update(data) {
+ return formatProjects(data.organization.projects.nodes);
+ },
+ error(error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.projects.loading;
+ },
+ emptyStateProps() {
+ const baseProps = {
+ svgHeight: 144,
+ svgPath: this.projectsEmptyStateSvgPath,
+ title: this.$options.i18n.emptyState.title,
+ description: this.$options.i18n.emptyState.description,
+ };
+
+ if (this.shouldShowEmptyStateButtons && this.newProjectPath) {
+ return {
+ ...baseProps,
+ primaryButtonLink: this.newProjectPath,
+ primaryButtonText: this.$options.i18n.emptyState.primaryButtonText,
+ };
+ }
+
+ return baseProps;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
+ <projects-list v-else-if="projects.length" :projects="projects" show-project-icon />
+ <gl-empty-state v-else v-bind="emptyStateProps" />
+</template>
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
index 842c601e326..842c601e326 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/groups.query.graphql
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
index 2a7971e1106..2a7971e1106 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/projects.query.graphql
diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 8a375b28797..c78266b0476 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,8 +1,4 @@
-import {
- organization,
- organizationProjects,
- organizationGroups,
-} from 'jest/organizations/groups_and_projects/mock_data';
+import { organization, organizationProjects, organizationGroups } from '../../mock_data';
export default {
Query: {
diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/shared/utils.js
index d2a4e05e806..c1aafefc553 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/utils.js
+++ b/app/assets/javascripts/organizations/shared/utils.js
@@ -1,5 +1,5 @@
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
export const formatProjects = (projects) =>
projects.map(({ id, nameWithNamespace, accessLevel, webUrl, ...project }) => ({
@@ -13,11 +13,14 @@ export const formatProjects = (projects) =>
},
webUrl,
editPath: `${webUrl}/edit`,
- actions: [ACTION_EDIT, ACTION_DELETE],
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
export const formatGroups = (groups) =>
- groups.map(({ id, ...group }) => ({
+ groups.map(({ id, webUrl, ...group }) => ({
...group,
id: getIdFromGraphQLId(id),
+ webUrl,
+ editPath: `${webUrl}/-/edit`,
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
diff --git a/app/assets/javascripts/organizations/show/components/app.vue b/app/assets/javascripts/organizations/show/components/app.vue
new file mode 100644
index 00000000000..47264d80454
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/app.vue
@@ -0,0 +1,37 @@
+<script>
+import OrganizationAvatar from './organization_avatar.vue';
+import GroupsAndProjects from './groups_and_projects.vue';
+import AssociationCounts from './association_counts.vue';
+
+export default {
+ name: 'OrganizationShowApp',
+ components: { OrganizationAvatar, GroupsAndProjects, AssociationCounts },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ associationCounts: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-py-6">
+ <organization-avatar :organization="organization" />
+ <association-counts
+ :association-counts="associationCounts"
+ :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
+ />
+ <groups-and-projects
+ :groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/association_count_card.vue b/app/assets/javascripts/organizations/show/components/association_count_card.vue
new file mode 100644
index 00000000000..0567f43132f
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/association_count_card.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlIcon, GlLink, GlCard } from '@gitlab/ui';
+import { numberToMetricPrefix } from '~/lib/utils/number_utils';
+import { __ } from '~/locale';
+
+export default {
+ name: 'AssociationCountCard',
+ components: { GlIcon, GlLink, GlCard },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ iconName: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Number,
+ required: true,
+ },
+ linkHref: {
+ type: String,
+ required: true,
+ },
+ linkText: {
+ type: String,
+ required: false,
+ default: __('View all'),
+ },
+ },
+ computed: {
+ formattedCount() {
+ return numberToMetricPrefix(this.count, 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card>
+ <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
+ <div class="gl-display-flex gl-align-items-center gl-text-gray-700">
+ <gl-icon :name="iconName" />
+ <span class="gl-ml-2">{{ title }}</span>
+ </div>
+ <gl-link :href="linkHref">{{ linkText }}</gl-link>
+ </div>
+ <span
+ class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-mt-2 gl-display-block"
+ >{{ formattedCount }}</span
+ >
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/association_counts.vue b/app/assets/javascripts/organizations/show/components/association_counts.vue
new file mode 100644
index 00000000000..3b312924bd2
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/association_counts.vue
@@ -0,0 +1,71 @@
+<script>
+import { __, s__ } from '~/locale';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import AssociationCountCard from './association_count_card.vue';
+
+export default {
+ name: 'AssociationCounts',
+ i18n: {
+ groups: __('Groups'),
+ projects: __('Projects'),
+ users: __('Users'),
+ viewAll: __('View all'),
+ manage: s__('Organization|Manage'),
+ },
+ components: { AssociationCountCard },
+ props: {
+ associationCounts: {
+ type: Object,
+ required: true,
+ },
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ groupsLinkHref() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${RESOURCE_TYPE_GROUPS}`;
+ },
+ projectsLinkHref() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${RESOURCE_TYPE_PROJECTS}`;
+ },
+ associationCountCards() {
+ return [
+ {
+ title: this.$options.i18n.groups,
+ iconName: 'group',
+ count: this.associationCounts.groups,
+ linkHref: this.groupsLinkHref,
+ },
+ {
+ title: this.$options.i18n.projects,
+ iconName: 'project',
+ count: this.associationCounts.projects,
+ linkHref: this.projectsLinkHref,
+ },
+ {
+ title: this.$options.i18n.users,
+ iconName: 'users',
+ count: this.associationCounts.users,
+ linkText: this.$options.i18n.manage,
+ // TODO: update `linkHref` prop to point to users route
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/409313
+ linkHref: '/',
+ },
+ ];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-grid gl-lg-grid-template-columns-4 gl-mt-5 gl-gap-5">
+ <association-count-card
+ v-for="props in associationCountCards"
+ :key="props.title"
+ v-bind="props"
+ class="gl-w-full"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/groups_and_projects.vue b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
new file mode 100644
index 00000000000..e8972f3b380
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/groups_and_projects.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import { s__, __ } from '~/locale';
+import GroupsView from '../../shared/components/groups_view.vue';
+import ProjectsView from '../../shared/components/projects_view.vue';
+import { RESOURCE_TYPE_GROUPS, RESOURCE_TYPE_PROJECTS } from '../../constants';
+import { FILTER_FREQUENTLY_VISITED } from '../constants';
+import { buildDisplayListboxItem } from '../utils';
+
+export default {
+ name: 'OrganizationFrontPageGroupsAndProjects',
+ i18n: {
+ displayListboxLabel: __('Display'),
+ viewAll: s__('Organization|View all'),
+ },
+ displayListboxLabelId: 'display-listbox-label',
+ components: { GlCollapsibleListbox, GlLink },
+ displayListboxItems: [
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_PROJECTS,
+ text: s__('Organization|Frequently visited projects'),
+ }),
+ buildDisplayListboxItem({
+ filter: FILTER_FREQUENTLY_VISITED,
+ resourceType: RESOURCE_TYPE_GROUPS,
+ text: s__('Organization|Frequently visited groups'),
+ }),
+ ],
+ props: {
+ groupsAndProjectsOrganizationPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ displayListboxSelected() {
+ const { display } = this.$route.query;
+ const [{ value: fallbackSelected }] = this.$options.displayListboxItems;
+
+ return (
+ this.$options.displayListboxItems.find(({ value }) => value === display)?.value ||
+ fallbackSelected
+ );
+ },
+ resourceTypeSelected() {
+ return [RESOURCE_TYPE_PROJECTS, RESOURCE_TYPE_GROUPS].find((resourceType) =>
+ this.displayListboxSelected.endsWith(resourceType),
+ );
+ },
+ routerView() {
+ switch (this.resourceTypeSelected) {
+ case RESOURCE_TYPE_GROUPS:
+ return GroupsView;
+
+ case RESOURCE_TYPE_PROJECTS:
+ return ProjectsView;
+
+ default:
+ return ProjectsView;
+ }
+ },
+ groupsAndProjectsOrganizationPathWithQueryParam() {
+ return `${this.groupsAndProjectsOrganizationPath}?display=${this.resourceTypeSelected}`;
+ },
+ },
+ methods: {
+ pushQuery(query) {
+ const currentQuery = this.$route.query;
+
+ if (isEqual(currentQuery, query)) {
+ return;
+ }
+
+ this.$router.push({ query });
+ },
+ onDisplayListboxSelect(display) {
+ this.pushQuery({ display });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mt-7">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div>
+ <label
+ :id="$options.displayListboxLabelId"
+ class="gl-display-block gl-mb-2"
+ data-testid="label"
+ >{{ $options.i18n.displayListboxLabel }}</label
+ >
+ <gl-collapsible-listbox
+ block
+ toggle-class="gl-w-30"
+ :selected="displayListboxSelected"
+ :items="$options.displayListboxItems"
+ :toggle-aria-labelled-by="$options.displayListboxLabelId"
+ @select="onDisplayListboxSelect"
+ />
+ </div>
+ <gl-link class="gl-mt-5" :href="groupsAndProjectsOrganizationPathWithQueryParam">{{
+ $options.i18n.viewAll
+ }}</gl-link>
+ </div>
+ <component :is="routerView" should-show-empty-state-buttons class="gl-mt-5" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
new file mode 100644
index 00000000000..c57ee0ea5b5
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlAvatar, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import {
+ VISIBILITY_TYPE_ICON,
+ ORGANIZATION_VISIBILITY_TYPE,
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+} from '~/visibility_level/constants';
+
+export default {
+ name: 'OrganizationAvatar',
+ AVATAR_SHAPE_OPTION_RECT,
+ i18n: {
+ copyButtonText: s__('Organization|Copy organization ID'),
+ orgId: s__('Organization|Org ID'),
+ },
+ components: { GlAvatar, GlIcon, ClipboardButton },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[VISIBILITY_LEVEL_PUBLIC_STRING];
+ },
+ visibilityTooltip() {
+ return ORGANIZATION_VISIBILITY_TYPE[VISIBILITY_LEVEL_PUBLIC_STRING];
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-avatar
+ :entity-id="organization.id"
+ :entity-name="organization.name"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="64"
+ />
+ <div class="gl-ml-3">
+ <div class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-m-0 gl-font-size-h1">{{ organization.name }}</h1>
+ <gl-icon
+ v-gl-tooltip="visibilityTooltip"
+ :name="visibilityIcon"
+ class="gl-text-secondary gl-ml-3"
+ />
+ </div>
+ <div class="gl-display-flex gl-align-items-center">
+ <span class="gl-text-secondary gl-font-sm"
+ >{{ $options.i18n.orgId }}: {{ organization.id }}</span
+ >
+ <clipboard-button
+ class="gl-ml-2"
+ category="tertiary"
+ size="small"
+ :title="$options.i18n.copyButtonText"
+ :text="organization.id.toString()"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/show/constants.js b/app/assets/javascripts/organizations/show/constants.js
new file mode 100644
index 00000000000..fe29af67f6b
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/constants.js
@@ -0,0 +1 @@
+export const FILTER_FREQUENTLY_VISITED = 'frequently_visited';
diff --git a/app/assets/javascripts/organizations/show/index.js b/app/assets/javascripts/organizations/show/index.js
new file mode 100644
index 00000000000..83a9c37e325
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/index.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import createDefaultClient from '~/lib/graphql';
+import { ORGANIZATION_ROOT_ROUTE_NAME } from '../constants';
+import resolvers from '../shared/graphql/resolvers';
+import App from './components/app.vue';
+
+export const createRouter = () => {
+ const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }];
+
+ const router = new VueRouter({
+ routes,
+ base: '/',
+ mode: 'history',
+ });
+
+ return router;
+};
+
+export const initOrganizationsShow = () => {
+ const el = document.getElementById('js-organizations-show');
+
+ if (!el) return false;
+
+ const {
+ dataset: { appData },
+ } = el;
+ const {
+ organization,
+ groupsAndProjectsOrganizationPath,
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ associationCounts,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
+
+ Vue.use(VueRouter);
+ const router = createRouter();
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
+
+ return new Vue({
+ el,
+ name: 'OrganizationShowRoot',
+ apolloProvider,
+ router,
+ provide: {
+ projectsEmptyStateSvgPath,
+ groupsEmptyStateSvgPath,
+ newGroupPath,
+ newProjectPath,
+ },
+ render(createElement) {
+ return createElement(App, {
+ props: { organization, groupsAndProjectsOrganizationPath, associationCounts },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/organizations/show/utils.js b/app/assets/javascripts/organizations/show/utils.js
new file mode 100644
index 00000000000..b4f935563aa
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/utils.js
@@ -0,0 +1,4 @@
+export const buildDisplayListboxItem = ({ filter, resourceType, text }) => ({
+ text,
+ value: `${filter}_${resourceType}`,
+});
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
index afddf78203d..7040f42398e 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
@@ -4,7 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
-import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import { apolloProvider } from './graphql/index';
import RegistryExplorer from './pages/index.vue';
import createRouter from './router';
@@ -88,7 +88,7 @@ export default () => {
});
return {
- attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
attachMainComponent,
};
};
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
index e18e6f7ed1a..6bb4a8797df 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue
@@ -2,8 +2,8 @@
import {
GlAlert,
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlFormGroup,
GlFormInputGroup,
GlSkeletonLoader,
@@ -18,6 +18,8 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
+import { getPageParams } from '~/packages_and_registries/dependency_proxy/utils';
+import { extractPageInfo } from '~/packages_and_registries/shared/utils';
import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql';
@@ -25,8 +27,8 @@ export default {
components: {
GlAlert,
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlSkeletonLoader,
GlFormGroup,
GlFormInputGroup,
@@ -79,11 +81,15 @@ export default {
},
computed: {
queryVariables() {
- return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE };
+ return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE, ...this.pageParams };
},
pageInfo() {
return this.group.dependencyProxyManifests?.pageInfo;
},
+ pageParams() {
+ const pageInfo = extractPageInfo(this.$route.query);
+ return getPageParams(pageInfo);
+ },
manifests() {
return this.group.dependencyProxyManifests?.nodes ?? [];
},
@@ -123,25 +129,10 @@ export default {
},
methods: {
fetchNextPage() {
- this.fetchMore({
- first: GRAPHQL_PAGE_SIZE,
- after: this.pageInfo?.endCursor,
- });
+ this.$router.push({ query: { after: this.pageInfo?.endCursor } });
},
fetchPreviousPage() {
- this.fetchMore({
- first: null,
- last: GRAPHQL_PAGE_SIZE,
- before: this.pageInfo?.startCursor,
- });
- },
- fetchMore(variables) {
- this.$apollo.queries.group.fetchMore({
- variables: { ...this.queryVariables, ...variables },
- updateQuery(_, { fetchMoreResult }) {
- return fetchMoreResult;
- },
- });
+ this.$router.push({ query: { before: this.pageInfo?.startCursor } });
},
async submit() {
try {
@@ -165,20 +156,23 @@ export default {
</gl-alert>
<title-area :title="$options.i18n.pageTitle">
<template #right-actions>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="showDeleteDropdown"
icon="ellipsis_v"
- text="More actions"
+ :toggle-text="__('More actions')"
:text-sr-only="true"
category="tertiary"
+ placement="right"
no-caret
>
- <gl-dropdown-item
- v-gl-modal-directive="$options.confirmClearCacheModal"
- variant="danger"
- >{{ $options.i18n.clearCache }}</gl-dropdown-item
- >
- </gl-dropdown>
+ <gl-disclosure-dropdown-item v-gl-modal-directive="$options.confirmClearCacheModal">
+ <template #list-item>
+ <span class="gl-text-red-500">
+ {{ $options.i18n.clearCache }}
+ </span>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<gl-button
v-if="canClearCache"
v-gl-tooltip="$options.i18n.settingsText"
@@ -198,14 +192,14 @@ export default {
<gl-form-input-group
id="proxy-url"
readonly
- :value="group.dependencyProxyImagePrefix"
+ :value="dependencyProxyImagePrefix"
select-on-click
class="gl-layout-w-limited"
data-testid="proxy-url"
>
<template #append>
<clipboard-button
- :text="group.dependencyProxyImagePrefix"
+ :text="dependencyProxyImagePrefix"
:title="$options.i18n.copyImagePrefixText"
/>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
index 74444d2c7ec..c115898c75b 100644
--- a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
-import app from '~/packages_and_registries/dependency_proxy/app.vue';
import { apolloProvider } from '~/packages_and_registries/dependency_proxy/graphql';
import Translate from '~/vue_shared/translate';
+import createRouter from './router';
Vue.use(Translate);
@@ -11,10 +11,18 @@ export const initDependencyProxyApp = () => {
if (!el) {
return null;
}
- const { groupPath, groupId, noManifestsIllustration, canClearCache, settingsPath } = el.dataset;
+ const {
+ endpoint,
+ groupPath,
+ groupId,
+ noManifestsIllustration,
+ canClearCache,
+ settingsPath,
+ } = el.dataset;
return new Vue({
el,
apolloProvider,
+ router: createRouter(endpoint),
provide: {
groupPath,
groupId,
@@ -23,7 +31,7 @@ export const initDependencyProxyApp = () => {
settingsPath,
},
render(createElement) {
- return createElement(app);
+ return createElement('router-view');
},
});
};
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/router.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/router.js
new file mode 100644
index 00000000000..087d8c189c4
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/router.js
@@ -0,0 +1,14 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import App from '~/packages_and_registries/dependency_proxy/app.vue';
+
+Vue.use(VueRouter);
+
+export default function createRouter(base) {
+ const routes = [{ path: '/', name: 'dependencyProxyApp', component: App }];
+ return new VueRouter({
+ mode: 'history',
+ base,
+ routes,
+ });
+}
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js
new file mode 100644
index 00000000000..e6b97fac896
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/utils.js
@@ -0,0 +1,24 @@
+import { GRAPHQL_PAGE_SIZE } from './constants';
+
+const getNextPageParams = (cursor) => ({
+ after: cursor,
+ first: GRAPHQL_PAGE_SIZE,
+});
+
+const getPreviousPageParams = (cursor) => ({
+ first: null,
+ before: cursor,
+ last: GRAPHQL_PAGE_SIZE,
+});
+
+export const getPageParams = (pageInfo = {}) => {
+ if (pageInfo.before) {
+ return getPreviousPageParams(pageInfo.before);
+ }
+
+ if (pageInfo.after) {
+ return getNextPageParams(pageInfo.after);
+ }
+
+ return {};
+};
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
index 6185e4c7bc6..41a5a0e3797 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
@@ -4,7 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from '~/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue';
-import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import createRouter from './router';
import HarborRegistryExplorer from './pages/index.vue';
@@ -79,7 +79,7 @@ export default (id) => {
};
return {
- attachBreadcrumb: renderBreadcrumb(router, null, RegistryBreadcrumb),
+ attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb),
attachMainComponent,
};
};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 0cf49b25bf2..1020cd0c533 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -6,9 +6,7 @@ import {
TOKEN_TITLE_TYPE,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PackageTypeToken from './tokens/package_type_token.vue';
@@ -24,7 +22,10 @@ export default {
operators: OPERATORS_IS,
},
],
- components: { RegistrySearch, UrlSync, LocalStorageSync },
+ components: {
+ LocalStorageSync,
+ PersistedSearch,
+ },
inject: ['isGroupPage'],
data() {
return {
@@ -40,17 +41,25 @@ export default {
sortableFields() {
return sortableFields(this.isGroupPage);
},
- parsedSorting() {
- const cleanOrderBy = this.sorting?.orderBy.replace('_at', '');
- return `${cleanOrderBy}_${this.sorting?.sort}`.toUpperCase();
- },
- parsedFilters() {
+ },
+ mounted() {
+ // local-storage-sync does not emit `input`
+ // event when key is not found, so set the
+ // flag if it hasn't been updated
+ this.$nextTick(() => {
+ if (!this.mountRegistrySearch) {
+ this.mountRegistrySearch = true;
+ }
+ });
+ },
+ methods: {
+ formatFilters(filters) {
const parsed = {
packageName: '',
packageType: undefined,
};
- return this.filters.reduce((acc, filter) => {
+ return filters.reduce((acc, filter) => {
if (filter.type === TOKEN_TYPE_TYPE && filter.value?.data) {
return {
...acc,
@@ -68,28 +77,17 @@ export default {
return acc;
}, parsed);
},
- },
- mounted() {
- const queryParams = getQueryParams(window.document.location.search);
- const { sorting, filters } = extractFilterAndSorting(queryParams);
- this.updateSorting(sorting);
- this.updateFilters(filters);
- this.mountRegistrySearch = true;
- this.emitUpdate();
- },
- methods: {
- updateFilters(newValue) {
- this.filters = newValue;
- },
updateSorting(newValue) {
this.sorting = { ...this.sorting, ...newValue };
},
- updateSortingAndEmitUpdate(newValue) {
+ updateSortingFromLocalStorage(newValue) {
this.updateSorting(newValue);
- this.emitUpdate();
+ this.mountRegistrySearch = true;
},
- emitUpdate() {
- this.$emit('update', { sort: this.parsedSorting, filters: this.parsedFilters });
+ emitUpdate(values) {
+ const { filters, sorting } = values;
+ this.updateSorting(sorting);
+ this.$emit('update', { ...values, filters: this.formatFilters(filters) });
},
},
};
@@ -99,22 +97,15 @@ export default {
<local-storage-sync
storage-key="package_registry_list_sorting"
:value="sorting"
- @input="updateSorting"
+ @input="updateSortingFromLocalStorage"
>
- <url-sync>
- <template #default="{ updateQuery }">
- <registry-search
- v-if="mountRegistrySearch"
- :filters="filters"
- :sorting="sorting"
- :tokens="$options.tokens"
- :sortable-fields="sortableFields"
- @sorting:changed="updateSortingAndEmitUpdate"
- @filter:changed="updateFilters"
- @filter:submit="emitUpdate"
- @query:changed="updateQuery"
- />
- </template>
- </url-sync>
+ <persisted-search
+ v-if="mountRegistrySearch"
+ :sortable-fields="sortableFields"
+ :default-order="sorting.orderBy"
+ :default-sort="sorting.sort"
+ :tokens="$options.tokens"
+ @update="emitUpdate"
+ />
</local-storage-sync>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index a7831ef2588..b892305055c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -48,10 +48,6 @@ export default {
required: false,
default: false,
},
- pageInfo: {
- type: Object,
- required: true,
- },
groupSettings: {
type: Object,
required: false,
@@ -179,11 +175,8 @@ export default {
:hidden-delete="!canDeletePackages"
:is-loading="isLoading"
:items="list"
- :pagination="pageInfo"
:title="listTitle"
@delete="setItemsToBeDeleted"
- @prev-page="$emit('prev-page')"
- @next-page="$emit('next-page')"
>
<template #default="{ selectItem, isSelected, item, first }">
<packages-list-row
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js
index ae0f6d18d99..1a9b192e2c8 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js
@@ -4,7 +4,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
-import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
+import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs';
import createRouter from './router';
Vue.use(Translate);
@@ -60,7 +60,7 @@ export default () => {
});
return {
- attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachBreadcrumb: () => injectVueAppBreadcrumbs(router, RegistryBreadcrumb, apolloProvider),
attachMainComponent,
};
};
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 6de89748708..a187c7a70d2 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
@@ -18,6 +18,12 @@ import DeletePackages from '~/packages_and_registries/package_registry/component
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
+import PersistedPagination from '~/packages_and_registries/shared/components/persisted_pagination.vue';
+import {
+ getPageParams,
+ getNextPageParams,
+ getPreviousPageParams,
+} from '~/packages_and_registries/package_registry/utils';
export default {
components: {
@@ -28,6 +34,7 @@ export default {
PackageList,
PackageTitle,
PackageSearch,
+ PersistedPagination,
DeletePackages,
},
directives: {
@@ -39,7 +46,7 @@ export default {
packagesResource: {},
sort: '',
filters: {},
- mutationLoading: false,
+ isDeleteInProgress: false,
pageParams: {},
};
},
@@ -100,7 +107,7 @@ export default {
: this.$options.i18n.noResultsTitle;
},
isLoading() {
- return this.$apollo.queries.packagesResource.loading || this.mutationLoading;
+ return this.$apollo.queries.packagesResource.loading || this.isDeleteInProgress;
},
refetchQueriesData() {
return [
@@ -124,23 +131,16 @@ export default {
historyReplaceState(cleanUrl);
}
},
- handleSearchUpdate({ sort, filters }) {
- this.pageParams = {};
+ handleSearchUpdate({ sort, filters, pageInfo }) {
+ this.pageParams = getPageParams(pageInfo);
this.sort = sort;
this.filters = { ...filters };
},
fetchNextPage() {
- this.pageParams = {
- first: GRAPHQL_PAGE_SIZE,
- after: this.pageInfo?.endCursor,
- };
+ this.pageParams = getNextPageParams(this.pageInfo.endCursor);
},
fetchPreviousPage() {
- this.pageParams = {
- first: null,
- last: GRAPHQL_PAGE_SIZE,
- before: this.pageInfo?.startCursor,
- };
+ this.pageParams = getPreviousPageParams(this.pageInfo.startCursor);
},
},
i18n: {
@@ -176,17 +176,14 @@ export default {
<delete-packages
:refetch-queries="refetchQueriesData"
show-success-alert
- @start="mutationLoading = true"
- @end="mutationLoading = false"
+ @start="isDeleteInProgress = true"
+ @end="isDeleteInProgress = false"
>
<template #default="{ deletePackages }">
<package-list
:group-settings="groupSettings"
:list="packages.nodes"
:is-loading="isLoading"
- :page-info="pageInfo"
- @prev-page="fetchPreviousPage"
- @next-page="fetchNextPage"
@delete="deletePackages"
>
<template #empty-state>
@@ -210,5 +207,13 @@ export default {
</package-list>
</template>
</delete-packages>
+ <div v-if="!isDeleteInProgress" class="gl-display-flex gl-justify-content-center">
+ <persisted-pagination
+ class="gl-mt-3"
+ :pagination="pageInfo"
+ @prev="fetchPreviousPage"
+ @next="fetchNextPage"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/utils.js b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
index 4ff8edb8f66..35ff3d5ea63 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/utils.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/utils.js
@@ -1,6 +1,7 @@
import { capitalize } from 'lodash';
import { s__ } from '~/locale';
import {
+ GRAPHQL_PAGE_SIZE,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
PACKAGE_TYPE_NPM,
@@ -46,3 +47,26 @@ export const packageTypeToTrackCategory = (type) => `UI::${capitalize(type)}Pack
export const sortableFields = (isGroupPage) =>
SORT_FIELDS.filter((f) => f.orderBy !== LIST_KEY_PROJECT || isGroupPage);
+
+export const getNextPageParams = (cursor) => ({
+ after: cursor,
+ first: GRAPHQL_PAGE_SIZE,
+});
+
+export const getPreviousPageParams = (cursor) => ({
+ first: null,
+ before: cursor,
+ last: GRAPHQL_PAGE_SIZE,
+});
+
+export const getPageParams = (pageInfo = {}) => {
+ if (pageInfo.before) {
+ return getPreviousPageParams(pageInfo.before);
+ }
+
+ if (pageInfo.after) {
+ return getNextPageParams(pageInfo.after);
+ }
+
+ return {};
+};
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue b/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
index dc61f3c788c..e00681f4183 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/cli_commands.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import Tracking from '~/tracking';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
@@ -16,8 +16,8 @@ const trackingLabel = 'quickstart_dropdown';
export default {
components: {
- GlDropdown,
CodeInstruction,
+ GlDisclosureDropdown,
},
mixins: [Tracking.mixin({ label: trackingLabel })],
props: {
@@ -47,14 +47,13 @@ export default {
};
</script>
<template>
- <gl-dropdown
- :text="$options.i18n.QUICK_START"
+ <gl-disclosure-dropdown
+ :toggle-text="$options.i18n.QUICK_START"
variant="confirm"
- right
+ placement="right"
@shown="track('click_dropdown')"
>
- <!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form -->
- <li role="presentation" class="px-2 py-1">
+ <div class="gl-px-3 gl-py-2">
<code-instruction
:label="$options.i18n.LOGIN_COMMAND_LABEL"
:instruction="dockerLoginCommand"
@@ -79,6 +78,6 @@ export default {
tracking-action="click_copy_push"
:tracking-label="$options.trackingLabel"
/>
- </li>
- </gl-dropdown>
+ </div>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
index 95343a3a09b..529086e7f8c 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
@@ -87,6 +87,7 @@ export default {
sort: this.parsedSorting,
filters: this.filters,
pageInfo: this.pageInfo,
+ sorting: this.sorting,
});
},
},
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index bda0839092e..a19c8ed5866 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -1,4 +1,3 @@
-import Vue from 'vue';
import { queryToObject } from '~/lib/utils/url_utility';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -47,28 +46,3 @@ export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGr
return `../commit/${pipeline.sha}`;
};
-
-export const renderBreadcrumb = (router, apolloProvider, RegistryBreadcrumb) => () => {
- const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
- const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
- const lastCrumb = breadCrumbEl.children[0];
- const crumbs = [lastCrumb];
- const nestedBreadcrumbEl = document.createElement('div');
- breadCrumbEl.replaceChild(nestedBreadcrumbEl, lastCrumb);
- return new Vue({
- el: nestedBreadcrumbEl,
- router,
- apolloProvider,
- components: {
- RegistryBreadcrumb,
- },
- render(createElement) {
- return createElement('registry-breadcrumb', {
- class: breadCrumbEl.className,
- props: {
- crumbs,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
deleted file mode 100644
index 29e92a8abad..00000000000
--- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import $ from 'jquery';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import { truncate } from '~/lib/utils/text_utility';
-
-const MAX_MESSAGE_LENGTH = 500;
-const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
-
-export default class AbuseReports {
- constructor() {
- $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
- $(document)
- .off('click', MESSAGE_CELL_SELECTOR)
- .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
- }
-
- truncateLongMessage() {
- const $messageCellElement = $(this);
- const reportMessage = $messageCellElement.text();
- if (reportMessage.length > MAX_MESSAGE_LENGTH) {
- $messageCellElement.data('originalMessage', reportMessage);
- $messageCellElement.data('messageTruncated', 'true');
- $messageCellElement.text(truncate(reportMessage, MAX_MESSAGE_LENGTH));
- }
- }
-
- toggleMessageTruncation() {
- const $messageCellElement = $(this);
- const originalMessage = $messageCellElement.data('originalMessage');
- if (!originalMessage) return;
- if (parseBoolean($messageCellElement.data('messageTruncated'))) {
- $messageCellElement.data('messageTruncated', 'false');
- $messageCellElement.text(originalMessage);
- } else {
- $messageCellElement.data('messageTruncated', 'true');
- $messageCellElement.text(truncate(originalMessage, MAX_MESSAGE_LENGTH));
- }
- }
-}
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index 7634f131e4d..78fef1b5531 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,10 +1,3 @@
import { initAbuseReportsApp } from '~/admin/abuse_reports';
-import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-import UsersSelect from '~/users_select';
-import AbuseReports from './abuse_reports';
-new AbuseReports(); /* eslint-disable-line no-new */
-new UsersSelect(); /* eslint-disable-line no-new */
-
-initDeprecatedRemoveRowBehavior();
initAbuseReportsApp();
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js
index 8a810ca649c..d0593c82ac1 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js
@@ -1,3 +1,4 @@
+import { initSilentModeSettings } from '~/silent_mode_settings';
import initAccountAndLimitsSection from '../account_and_limits';
import initGitpod from '../gitpod';
import initSignupRestrictions from '../signup_restrictions';
@@ -6,4 +7,5 @@ import initSignupRestrictions from '../signup_restrictions';
initAccountAndLimitsSection();
initGitpod();
initSignupRestrictions();
+ initSilentModeSettings();
})();
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql b/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql
deleted file mode 100644
index 8c59230b2b8..00000000000
--- a/app/assets/javascripts/pages/admin/jobs/components/table/graphql/queries/get_all_jobs_count.query.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-query getAllJobsCount($statuses: [CiJobStatus!]) {
- jobs(statuses: $statuses) {
- count
- }
-}
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 9c2a255a1a3..52e4d5dd19f 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -1,12 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
import createDefaultClient from '~/lib/graphql';
-import { CANCEL_JOBS_MODAL_ID } from '../components/constants';
-import CancelJobsModal from '../components/cancel_jobs_modal.vue';
-import AdminJobsTableApp from '../components/table/admin_jobs_table_app.vue';
-import cacheConfig from '../components/table/graphql/cache_config';
+import AdminJobsTableApp from '~/ci/admin/jobs_table/admin_jobs_table_app.vue';
+import cacheConfig from '~/ci/admin/jobs_table/graphql/cache_config';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -17,35 +14,7 @@ const apolloProvider = new VueApollo({
defaultClient: client,
});
-function initJobs() {
- const buttonId = 'js-stop-jobs-button';
- const cancelJobsButton = document.getElementById(buttonId);
- if (cancelJobsButton) {
- // eslint-disable-next-line no-new
- new Vue({
- el: `#js-${CANCEL_JOBS_MODAL_ID}`,
- components: {
- CancelJobsModal,
- },
- mounted() {
- cancelJobsButton.classList.remove('disabled');
- cancelJobsButton.addEventListener('click', () => {
- this.$root.$emit(BV_SHOW_MODAL, CANCEL_JOBS_MODAL_ID, `#${buttonId}`);
- });
- },
- render(createElement) {
- return createElement(CANCEL_JOBS_MODAL_ID, {
- props: {
- url: cancelJobsButton.dataset.url,
- modalId: CANCEL_JOBS_MODAL_ID,
- },
- });
- },
- });
- }
-}
-
-export function initAdminJobsApp() {
+const initAdminJobsApp = () => {
const containerEl = document.getElementById('admin-jobs-app');
if (!containerEl) return false;
@@ -64,10 +33,6 @@ export function initAdminJobsApp() {
return createElement(AdminJobsTableApp);
},
});
-}
+};
-if (gon.features.adminJobsVue) {
- initAdminJobsApp();
-} else {
- initJobs();
-}
+initAdminJobsApp();
diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js
index c14848c4798..fbfc6c07904 100644
--- a/app/assets/javascripts/pages/dashboard/groups/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js
@@ -1,3 +1,4 @@
+import EmptyState from '~/groups/components/empty_states/groups_dashboard_empty_state.vue';
import initGroupsList from '~/groups';
-initGroupsList();
+initGroupsList(EmptyState);
diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js
index 9b60b1f51a8..c6a9cf50d9a 100644
--- a/app/assets/javascripts/pages/explore/groups/index.js
+++ b/app/assets/javascripts/pages/explore/groups/index.js
@@ -1,8 +1,9 @@
+import EmptyState from '~/groups/components/empty_states/groups_explore_empty_state.vue';
import initGroupsList from '~/groups';
import Landing from '~/groups/landing';
function exploreGroups() {
- initGroupsList();
+ initGroupsList(EmptyState);
const landingElement = document.querySelector('.js-explore-groups-landing');
if (!landingElement) return;
const exploreGroupsLanding = new Landing(
diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
index f0c4ecbe3eb..3a73d0c9f40 100644
--- a/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
+++ b/app/assets/javascripts/pages/import/bitbucket_server/status/components/bitbucket_server_status_table.vue
@@ -19,9 +19,14 @@ export default {
<template>
<bitbucket-status-table v-bind="$attrs">
<template #actions>
- <gl-button variant="info" class="gl-ml-3" data-method="post" :href="reconfigurePath">{{
- __('Reconfigure')
- }}</gl-button>
+ <gl-button
+ category="secondary"
+ variant="confirm"
+ class="gl-ml-3"
+ data-method="post"
+ :href="reconfigurePath"
+ >{{ __('Reconfigure') }}</gl-button
+ >
</template>
</bitbucket-status-table>
</template>
diff --git a/app/assets/javascripts/pages/organizations/organizations/show/index.js b/app/assets/javascripts/pages/organizations/organizations/show/index.js
new file mode 100644
index 00000000000..f9f9d2db692
--- /dev/null
+++ b/app/assets/javascripts/pages/organizations/organizations/show/index.js
@@ -0,0 +1,3 @@
+import { initOrganizationsShow } from '~/organizations/show';
+
+initOrganizationsShow();
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index a83c4f1c0d2..a2ff45b454a 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,7 +1,3 @@
import { initShow } from '~/issues';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initWorkItemLinks from '~/work_items/components/work_item_links';
initShow();
-initSidebarBundle();
-initWorkItemLinks();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 0844e322de2..ead15143072 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,5 +1,5 @@
import { initFilteredSearchServiceDesk } from '~/issues';
-import { mountServiceDeskListApp } from '~/service_desk';
+import { mountServiceDeskListApp } from '~/issues/service_desk';
initFilteredSearchServiceDesk();
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index c92958cd8c7..a2ff45b454a 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,10 +1,3 @@
import { initShow } from '~/issues';
-import { store } from '~/notes/stores';
-import { initRelatedIssues } from '~/related_issues';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initWorkItemLinks from '~/work_items/components/work_item_links';
initShow();
-initSidebarBundle(store);
-initRelatedIssues();
-initWorkItemLinks();
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index eb3a24f38a8..9b5ad804750 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -1,3 +1,3 @@
-import initJobsTable from '~/jobs/components/table';
+import initJobsPage from '~/ci/jobs_page';
-initJobsTable();
+initJobsPage();
diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js
index 6fef057dee0..cd83f2b7b64 100644
--- a/app/assets/javascripts/pages/projects/jobs/show/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/show/index.js
@@ -1,3 +1,3 @@
-import initJobDetails from '~/jobs';
+import initJobDetails from '~/ci/job_details';
initJobDetails();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js
new file mode 100644
index 00000000000..ee84f54978a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/branch_finder.js
@@ -0,0 +1 @@
+export const findTargetBranch = async () => {};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index f71a1041068..d23a0615bb8 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -2,6 +2,8 @@ import Vue from 'vue';
import { mountMarkdownEditor } from 'ee_else_ce/vue_shared/components/markdown/mount_markdown_editor';
+import { findTargetBranch } from 'ee_else_ce/pages/projects/merge_requests/creations/new/branch_finder';
+
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import MergeRequest from '~/merge_request';
import CompareApp from '~/merge_requests/components/compare_app.vue';
@@ -13,14 +15,15 @@ if (mrNewCompareNode) {
const targetCompareEl = document.getElementById('js-target-project-dropdown');
const sourceCompareEl = document.getElementById('js-source-project-dropdown');
const compareEl = document.querySelector('.js-merge-request-new-compare');
+ const targetBranch = Vue.observable({ name: '' });
+ const currentSourceBranch = JSON.parse(sourceCompareEl.dataset.currentBranch);
// eslint-disable-next-line no-new
new Vue({
el: sourceCompareEl,
name: 'SourceCompareApp',
provide: {
currentProject: JSON.parse(sourceCompareEl.dataset.currentProject),
- currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch),
branchCommitPath: compareEl.dataset.sourceBranchUrl,
inputs: {
project: {
@@ -40,20 +43,35 @@ if (mrNewCompareNode) {
project: 'js-source-project',
branch: 'js-source-branch gl-font-monospace',
},
- branchQaSelector: 'source_branch_dropdown',
+ },
+ methods: {
+ async selectedBranch(branchName) {
+ const targetBranchName = await findTargetBranch(branchName);
+
+ if (targetBranchName) {
+ targetBranch.name = targetBranchName;
+ }
+ },
},
render(h) {
- return h(CompareApp);
+ return h(CompareApp, {
+ props: {
+ currentBranch: currentSourceBranch,
+ },
+ on: {
+ 'select-branch': this.selectedBranch,
+ },
+ });
},
});
+ const currentTargetBranch = JSON.parse(targetCompareEl.dataset.currentBranch);
// eslint-disable-next-line no-new
new Vue({
el: targetCompareEl,
name: 'TargetCompareApp',
provide: {
currentProject: JSON.parse(targetCompareEl.dataset.currentProject),
- currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch),
projectsPath: targetCompareEl.dataset.targetProjectsPath,
branchCommitPath: compareEl.dataset.targetBranchUrl,
inputs: {
@@ -75,8 +93,17 @@ if (mrNewCompareNode) {
branch: 'js-target-branch gl-font-monospace',
},
},
+ computed: {
+ currentBranch() {
+ if (targetBranch.name) {
+ return { text: targetBranch.name, value: targetBranch.name };
+ }
+
+ return currentTargetBranch;
+ },
+ },
render(h) {
- return h(CompareApp);
+ return h(CompareApp, { props: { currentBranch: this.currentBranch } });
},
});
} else {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index 30734f0b698..2cdbf0fb830 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -4,7 +4,7 @@ import { s__ } from '~/locale';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import { initIssuableSidebar } from '~/issuable';
-import StatusBox from '~/issuable/components/status_box.vue';
+import MergeRequestStatusBadge from '~/merge_requests/components/merge_request_status_badge.vue';
import createDefaultClient from '~/lib/graphql';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
@@ -24,24 +24,24 @@ export default function initMergeRequestShow() {
initMrExperienceSurvey();
const el = document.querySelector('.js-mr-status-box');
- const { iid, issuableType, projectPath } = el.dataset;
- const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
- });
+ const { iid, issuableType, projectPath, state } = el.dataset;
+
// eslint-disable-next-line no-new
new Vue({
el,
name: 'IssuableStatusBoxRoot',
- apolloProvider,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient(),
+ }),
provide: {
query: getStateQuery,
iid,
projectPath,
},
- render(h) {
- return h(StatusBox, {
+ render(createElement) {
+ return createElement(MergeRequestStatusBadge, {
props: {
- initialState: el.dataset.state,
+ initialState: state,
issuableType,
},
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index 75e308e706f..f7b522f7c85 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import initMrNotes from 'ee_else_ce/mr_notes';
+import { mountHeaderMetadata } from '~/merge_requests';
import StickyHeader from '~/merge_requests/components/sticky_header.vue';
-import { initIssuableHeaderWarnings } from '~/issuable';
import { start as startCodeReviewMessaging } from '~/code_review/signals';
import diffsEventHub from '~/diffs/event_hub';
import store from '~/mr_notes/stores';
@@ -24,7 +24,7 @@ export function initMrPage() {
requestIdleCallback(() => {
initSidebarBundle(store);
- initIssuableHeaderWarnings(store);
+ mountHeaderMetadata(store);
const el = document.getElementById('js-merge-sticky-header');
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
index a51c2e9c47b..f48c38b776f 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -1,8 +1,3 @@
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
-import initForm from '../shared/init_form';
-if (gon.features?.pipelineSchedulesVue) {
- initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
-} else {
- initForm();
-}
+initPipelineSchedulesFormApp('#pipeline-schedules-form-edit', true);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
index 4bdbb70d942..0eff9110412 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,75 +1,3 @@
-import Vue from 'vue';
-import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import initPipelineSchedulesApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_app';
-import PipelineSchedulesTakeOwnershipModalLegacy from '~/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue';
-import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-function initPipelineSchedulesCallout() {
- const el = document.getElementById('pipeline-schedules-callout');
-
- if (!el) {
- return;
- }
-
- const { docsUrl, illustrationUrl } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- name: 'PipelineSchedulesCalloutRoot',
- provide: {
- docsUrl,
- illustrationUrl,
- },
- render(createElement) {
- return createElement(PipelineSchedulesCallout);
- },
- });
-}
-
-// TODO: move take ownership feature into new Vue app
-// located in directory app/assets/javascripts/pipeline_schedules/components
-function initTakeownershipModal() {
- const modalId = 'pipeline-take-ownership-modal';
- const buttonSelector = 'js-take-ownership-button';
- const el = document.getElementById(modalId);
- const takeOwnershipButtons = document.querySelectorAll(`.${buttonSelector}`);
-
- if (!el) {
- return;
- }
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- data() {
- return {
- url: '',
- };
- },
- mounted() {
- takeOwnershipButtons.forEach((button) => {
- button.addEventListener('click', () => {
- const { url } = button.dataset;
-
- this.url = url;
- this.$root.$emit(BV_SHOW_MODAL, modalId, `.${buttonSelector}`);
- });
- });
- },
- render(createElement) {
- return createElement(PipelineSchedulesTakeOwnershipModalLegacy, {
- props: {
- ownershipUrl: this.url,
- },
- });
- },
- });
-}
-
-if (gon.features?.pipelineSchedulesVue) {
- initPipelineSchedulesApp();
-} else {
- initPipelineSchedulesCallout();
- initTakeownershipModal();
-}
+initPipelineSchedulesApp();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
index d8ba7bbd752..672e3d014bd 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -1,8 +1,3 @@
import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
-import initForm from '../shared/init_form';
-if (gon.features?.pipelineSchedulesVue) {
- initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
-} else {
- initForm();
-}
+initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 5f6a73782c3..642fd56eab1 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -69,7 +69,8 @@ export default {
formattedTime() {
if (this.randomHour > 12) {
return `${this.randomHour - 12}:00pm`;
- } else if (this.randomHour === 12) {
+ }
+ if (this.randomHour === 12) {
return `12:00pm`;
}
return `${this.randomHour}:00am`;
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
deleted file mode 100644
index b3ad50f395b..00000000000
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import Vue from 'vue';
-import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils';
-import Translate from '~/vue_shared/translate';
-
-Vue.use(Translate);
-
-const cookieKey = 'pipeline_schedules_callout_dismissed';
-
-export default {
- name: 'PipelineSchedulesCallout',
- components: {
- GlButton,
- },
- inject: ['docsUrl', 'illustrationUrl'],
- data() {
- return {
- calloutDismissed: parseBoolean(getCookie(cookieKey)),
- };
- },
- methods: {
- dismissCallout() {
- this.calloutDismissed = true;
- setCookie(cookieKey, this.calloutDismissed);
- },
- },
-};
-</script>
-<template>
- <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
- <div class="bordered-box landing content-block gl-p-5!" data-testid="innerContent">
- <gl-button
- category="tertiary"
- icon="close"
- :aria-label="__('Dismiss')"
- class="gl-absolute gl-top-2 gl-right-2"
- @click="dismissCallout"
- />
- <div class="svg-content">
- <img :src="illustrationUrl" />
- </div>
- <div class="user-callout-copy">
- <h4>{{ __('Scheduling Pipelines') }}</h4>
- <p>
- {{
- __(`The pipelines schedule runs pipelines in the future,
-repeatedly, for specific branches or tags.
-Those scheduled pipelines will inherit limited project access based on their associated user.`)
- }}
- </p>
- <p>
- {{ __('Learn more in the') }}
- <a :href="docsUrl" target="_blank" rel="nofollow">
- {{ __('pipeline schedules documentation') }}</a
- >.
- <!-- oneline to prevent extra space before period -->
- </p>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
deleted file mode 100644
index 8440d0e77cd..00000000000
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-import { __ } from '~/locale';
-import RefSelector from '~/ref/components/ref_selector.vue';
-import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
-import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
-import GlFieldErrors from '~/gl_field_errors';
-import Translate from '~/vue_shared/translate';
-import { initTimezoneDropdown } from '../../../profiles/init_timezone_dropdown';
-import IntervalPatternInput from './components/interval_pattern_input.vue';
-
-Vue.use(Translate);
-
-function initIntervalPatternInput() {
- const intervalPatternMount = document.getElementById('interval-pattern-input');
- const initialCronInterval = intervalPatternMount?.dataset?.initialInterval;
- const dailyLimit = intervalPatternMount.dataset?.dailyLimit;
-
- return new Vue({
- el: intervalPatternMount,
- components: {
- IntervalPatternInput,
- },
- render(createElement) {
- return createElement('interval-pattern-input', {
- props: {
- initialCronInterval,
- dailyLimit,
- },
- });
- },
- });
-}
-
-function getEnabledRefTypes() {
- return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
-}
-
-function initTargetRefDropdown() {
- const $refField = document.getElementById('schedule_ref');
- const el = document.querySelector('.js-target-ref-dropdown');
- const { projectId, defaultBranch } = el.dataset;
-
- if (!$refField.value) {
- $refField.value = defaultBranch;
- }
-
- const refDropdown = new Vue({
- el,
- render(h) {
- return h(RefSelector, {
- props: {
- enabledRefTypes: getEnabledRefTypes(),
- projectId,
- value: $refField.value,
- useSymbolicRefNames: true,
- translations: {
- dropdownHeader: __('Select target branch or tag'),
- },
- },
- class: 'gl-w-full',
- });
- },
- });
-
- refDropdown.$children[0].$on('input', (newRef) => {
- $refField.value = newRef;
- });
-
- return refDropdown;
-}
-
-export default () => {
- /* Most of the form is written in haml, but for fields with more complex behaviors,
- * you should mount individual Vue components here. If at some point components need
- * to share state, it may make sense to refactor the whole form to Vue */
-
- initIntervalPatternInput();
-
- // Initialize non-Vue JS components in the form
-
- const formElement = document.getElementById('new-pipeline-schedule-form');
-
- gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
-
- initTargetRefDropdown();
-
- setupNativeFormVariableList({
- container: $('.js-ci-variable-list-section'),
- formField: 'schedule',
- });
-};
-
-initTimezoneDropdown();
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
index 63b1f2bf975..a0ddf96ede2 100644
--- a/app/assets/javascripts/pages/projects/pipelines/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -1,3 +1,3 @@
-import { initPipelinesIndex } from '~/pipelines/pipelines_index';
+import { initPipelinesIndex } from '~/ci/pipeline_details/pipelines_index';
initPipelinesIndex();
diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js
index d3f46b7e025..225479c5194 100644
--- a/app/assets/javascripts/pages/projects/pipelines/show/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js
@@ -1,4 +1,4 @@
-import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
+import initPipelineDetails from '~/ci/pipeline_details/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
initPipelines();
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 4a5d5580c08..dce40c1f322 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -12,7 +12,6 @@ import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
import { initCiSecureFiles } from '~/ci_secure_files';
import initDeployTokens from '~/deploy_tokens';
-import { initProjectRunners } from '~/ci/runner/project_runners';
import { initProjectRunnersRegistrationDropdown } from '~/ci/runner/project_runners/register';
import { initGeneralPipelinesOptions } from '~/ci_settings_general_pipeline';
@@ -45,7 +44,6 @@ initDeployFreeze();
initSettingsPipelinesTriggers();
initArtifactsSettings();
-initProjectRunners();
initProjectRunnersRegistrationDropdown();
initSharedRunnersToggle();
initRefSwitcherBadges();
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index c43a0eb597c..bee0731d711 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,13 +1,11 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-
import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
-import Star from '~/projects/star';
+import { initStarButton } from '~/projects/project_star_button';
import initTerraformNotification from '~/projects/terraform_notification';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
-
import initForksButton from '~/forks/init_forks_button';
// Project show page loads different overview content based on user preferences
@@ -46,7 +44,7 @@ initClustersDeprecationAlert();
initTerraformNotification();
initReadMore();
-new Star(); // eslint-disable-line no-new
+initStarButton();
if (document.querySelector('.js-autodevops-banner')) {
import(/* webpackChunkName: 'userCallOut' */ '~/user_callout')
diff --git a/app/assets/javascripts/pages/projects/tracing/index/index.js b/app/assets/javascripts/pages/projects/tracing/index/index.js
deleted file mode 100644
index 64ca303f8ba..00000000000
--- a/app/assets/javascripts/pages/projects/tracing/index/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import ListIndex from '~/tracing/list_index.vue';
-
-initSimpleApp('#js-tracing', ListIndex);
diff --git a/app/assets/javascripts/pages/projects/tracing/show/index.js b/app/assets/javascripts/pages/projects/tracing/show/index.js
deleted file mode 100644
index 107c004aa5f..00000000000
--- a/app/assets/javascripts/pages/projects/tracing/show/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-import { initSimpleApp } from '~/helpers/init_simple_app_helper';
-import DetailsIndex from '~/tracing/details_index.vue';
-
-initSimpleApp('#js-tracing-details', DetailsIndex);
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 5bef7e6e322..b53e2709f83 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -67,7 +67,8 @@ export default {
metricDetailsLabel() {
if (this.metricDetails.duration && this.metricDetails.calls) {
return `${this.metricDetails.duration} / ${this.metricDetails.calls}`;
- } else if (this.metricDetails.calls) {
+ }
+ if (this.metricDetails.calls) {
return this.metricDetails.calls;
}
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index e7f2662ae09..cea01852630 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -19,13 +19,11 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-namespace-storage-alert',
'.js-web-hook-disabled-callout',
'.js-merge-request-settings-callout',
- '.js-ultimate-feature-removal-banner',
'.js-geo-enable-hashed-storage-callout',
'.js-geo-migrate-hashed-storage-callout',
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
'.js-new-navigation-callout',
- '.js-code-suggestions-third-party-callout',
'.js-namespace-over-storage-users-combined-alert',
];
diff --git a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js b/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js
deleted file mode 100644
index 578ff498358..00000000000
--- a/app/assets/javascripts/pipelines/mixins/stage_column_mixin.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default {
- props: {
- hasUpstream: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- methods: {
- buildConnnectorClass(index) {
- return index === 0 && (!this.isFirstColumn || this.hasUpstream) ? 'left-connector' : '';
- },
- },
-};
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index e21f8557d68..947bf7acd5c 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -30,7 +30,6 @@ export default class Profile {
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
}
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 6ff9bd7390f..b7355b909a1 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -2,16 +2,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
+import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
+import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
+import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
+import getLinkedPipelinesQuery from '~/ci/pipeline_details/graphql/queries/get_linked_pipelines.query.graphql';
+import getPipelineStagesQuery from '~/ci/pipeline_mini_graph/graphql/queries/get_pipeline_stages.query.graphql';
import { formatStages } from '../utils';
import { COMMIT_BOX_POLL_INTERVAL } from '../constants';
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 71f53613a3b..ccecc914cf1 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
@@ -2,10 +2,7 @@
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { createAlert } from '~/alert';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
+import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
import { COMMIT_BOX_POLL_INTERVAL, PIPELINE_STATUS_FETCH_ERROR } from '../constants';
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
deleted file mode 100644
index 034bae3066d..00000000000
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
+++ /dev/null
@@ -1,156 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- },
- props: {
- refsProjectPath: {
- type: String,
- required: true,
- },
- revisionText: {
- type: String,
- required: true,
- },
- paramsName: {
- type: String,
- required: true,
- },
- paramsBranch: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- branches: [],
- tags: [],
- loading: true,
- searchTerm: '',
- selectedRevision: this.getDefaultBranch(),
- };
- },
- computed: {
- filteredBranches() {
- return this.branches.filter((branch) =>
- branch.toLowerCase().includes(this.searchTerm.toLowerCase()),
- );
- },
- hasFilteredBranches() {
- return this.filteredBranches.length;
- },
- filteredTags() {
- return this.tags.filter((tag) => tag.toLowerCase().includes(this.searchTerm.toLowerCase()));
- },
- hasFilteredTags() {
- return this.filteredTags.length;
- },
- },
- watch: {
- paramsBranch(newBranch) {
- this.setSelectedRevision(newBranch);
- },
- },
- mounted() {
- this.fetchBranchesAndTags();
- },
- methods: {
- fetchBranchesAndTags() {
- const endpoint = this.refsProjectPath;
-
- return axios
- .get(endpoint)
- .then(({ data }) => {
- this.branches = data.Branches || [];
- this.tags = data.Tags || [];
- })
- .catch(() => {
- createAlert({
- message: `${s__(
- 'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
- )}`,
- });
- })
- .finally(() => {
- this.loading = false;
- });
- },
- getDefaultBranch() {
- return this.paramsBranch || s__('CompareRevisions|Select branch/tag');
- },
- onClick(revision) {
- this.setSelectedRevision(revision);
- },
- onSearchEnter() {
- this.setSelectedRevision(this.searchTerm);
- },
- setSelectedRevision(revision) {
- this.selectedRevision = revision || s__('CompareRevisions|Select branch/tag');
- this.$emit('selectRevision', { direction: this.paramsName, revision });
- },
- },
-};
-</script>
-
-<template>
- <div class="form-group compare-form-group" :class="`js-compare-${paramsName}-dropdown`">
- <div class="input-group inline-input-group">
- <span class="input-group-prepend">
- <div class="input-group-text">
- {{ revisionText }}
- </div>
- </span>
- <input type="hidden" :name="paramsName" :value="selectedRevision" />
- <gl-dropdown
- class="gl-flex-grow-1 gl-flex-basis-0 gl-min-w-0 gl-font-monospace"
- toggle-class="form-control compare-dropdown-toggle gl-min-w-0 gl-rounded-top-left-none! gl-rounded-bottom-left-none!"
- :text="selectedRevision"
- header-text="Select Git revision"
- :loading="loading"
- >
- <template #header>
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- :placeholder="s__('CompareRevisions|Filter by Git revision')"
- @keyup.enter="onSearchEnter"
- />
- </template>
- <gl-dropdown-section-header v-if="hasFilteredBranches">
- {{ s__('CompareRevisions|Branches') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="(branch, index) in filteredBranches"
- :key="`branch${index}`"
- is-check-item
- :is-checked="selectedRevision === branch"
- data-testid="branches-dropdown-item"
- @click="onClick(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- <gl-dropdown-section-header v-if="hasFilteredTags">
- {{ s__('CompareRevisions|Tags') }}
- </gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="(tag, index) in filteredTags"
- :key="`tag${index}`"
- is-check-item
- :is-checked="selectedRevision === tag"
- data-testid="tags-dropdown-item"
- @click="onClick(tag)"
- >
- {{ tag }}
- </gl-dropdown-item>
- </gl-dropdown>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index c13824a9952..dcec77ac6a4 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -21,5 +21,5 @@ export const LOAD_PIPELINES_FAILURE = 'load_analytics_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const SNOWPLOW_LABEL = 'redis_hll_counters.analytics.analytics_total_unique_counts_monthly';
-export const SNOWPLOW_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-0';
+export const SNOWPLOW_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1';
export const SNOWPLOW_DATA_SOURCE = 'redis_hll';
diff --git a/app/assets/javascripts/projects/project_star_button.js b/app/assets/javascripts/projects/project_star_button.js
new file mode 100644
index 00000000000..06f982b500d
--- /dev/null
+++ b/app/assets/javascripts/projects/project_star_button.js
@@ -0,0 +1,46 @@
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { __, s__ } from '~/locale';
+
+export function initStarButton(containerSelector = '.project-home-panel') {
+ const container = document.querySelector(containerSelector);
+ const starToggle = container?.querySelector('.toggle-star');
+
+ if (!starToggle) {
+ return;
+ }
+
+ starToggle.addEventListener('click', function toggleStarClickCallback() {
+ const starSpan = starToggle.querySelector('span');
+ const starIcon = starToggle.querySelector('svg');
+ const iconClasses = Array.from(starIcon.classList.values());
+
+ axios
+ .post(starToggle.dataset.endpoint)
+ .then(({ data }) => {
+ const isStarred = starSpan.classList.contains('starred');
+ starToggle.parentNode.querySelector('.count').textContent = data.star_count;
+
+ if (isStarred) {
+ starSpan.classList.remove('starred');
+ starSpan.textContent = s__('StarProject|Star');
+ starIcon.remove();
+ // eslint-disable-next-line no-unsanitized/method
+ starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
+ } else {
+ starSpan.classList.add('starred');
+ starSpan.textContent = __('Unstar');
+ starIcon.remove();
+
+ // eslint-disable-next-line no-unsanitized/method
+ starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
+ }
+ })
+ .catch(() =>
+ createAlert({
+ message: __('Star toggle failed. Try again later.'),
+ }),
+ );
+ });
+}
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
deleted file mode 100644
index 75d72f719e5..00000000000
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ /dev/null
@@ -1,611 +0,0 @@
-/* eslint-disable no-underscore-dangle, class-methods-use-this */
-import { escape, find, countBy } from 'lodash';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { createAlert } from '~/alert';
-import { n__, s__, __, sprintf } from '~/locale';
-import { renderAvatar } from '~/helpers/avatar_helper';
-import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
-import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
-
-export default class AccessDropdown {
- constructor(options) {
- const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
- this.options = options;
- this.hasLicense = hasLicense;
- this.groups = [];
- this.accessLevel = accessLevel;
- this.accessLevelsData = accessLevelsData.roles;
- this.$dropdown = $dropdown;
- this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
- this.defaultLabel = this.$dropdown.data('defaultLabel');
-
- this.setSelectedItems([]);
- this.persistPreselectedItems();
-
- this.noOneObj = this.accessLevelsData.find((level) => level.id === ACCESS_LEVEL_NONE);
-
- this.initDropdown();
- }
-
- initDropdown() {
- const { onSelect, onHide } = this.options;
- initDeprecatedJQueryDropdown(this.$dropdown, {
- data: this.getData.bind(this),
- selectable: true,
- filterable: true,
- filterRemote: true,
- multiSelect: this.$dropdown.hasClass('js-multiselect'),
- renderRow: this.renderRow.bind(this),
- toggleLabel: this.toggleLabel.bind(this),
- hidden() {
- if (onHide) {
- onHide();
- }
- },
- clicked: (options) => {
- const { $el, e } = options;
- const item = options.selectedObj;
- const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
-
- e.preventDefault();
-
- if (fossWithMergeAccess) {
- // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
- // remove all preselected items before selecting this item
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
- this.accessLevelsData.forEach((level) => {
- this.removeSelectedItem(level);
- });
- }
-
- if ($el.is('.is-active')) {
- if (this.noOneObj) {
- if (item.id === this.noOneObj.id && !fossWithMergeAccess) {
- // remove all others selected items
- this.accessLevelsData.forEach((level) => {
- if (level.id !== item.id) {
- this.removeSelectedItem(level);
- }
- });
-
- // remove selected item visually
- this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
- } else {
- const $noOne = this.$wrap.find(
- `.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
- );
- if ($noOne.length) {
- $noOne.removeClass('is-active');
- this.removeSelectedItem(this.noOneObj);
- }
- }
- }
-
- // make element active right away
- $el.addClass(`is-active item-${item.type}`);
-
- // Add "No one"
- this.addSelectedItem(item);
- } else {
- this.removeSelectedItem(item);
- }
-
- if (onSelect) {
- onSelect(item, $el, this);
- }
- },
- });
-
- this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
- }
-
- persistPreselectedItems() {
- const itemsToPreselect = this.$dropdown.data('preselectedItems');
-
- if (!itemsToPreselect || !itemsToPreselect.length) {
- return;
- }
-
- const persistedItems = itemsToPreselect.map((item) => {
- const persistedItem = { ...item };
- persistedItem.persisted = true;
- return persistedItem;
- });
-
- this.setSelectedItems(persistedItems);
- }
-
- setSelectedItems(items = []) {
- this.items = items;
- }
-
- getSelectedItems() {
- return this.items.filter((item) => !item._destroy);
- }
-
- getAllSelectedItems() {
- return this.items;
- }
-
- // Return dropdown as input data ready to submit
- getInputData() {
- const selectedItems = this.getAllSelectedItems();
-
- const accessLevels = selectedItems.map((item) => {
- const obj = {};
-
- if (typeof item.id !== 'undefined') {
- obj.id = item.id;
- }
-
- if (typeof item._destroy !== 'undefined') {
- obj._destroy = item._destroy;
- }
-
- if (item.type === LEVEL_TYPES.ROLE) {
- obj.access_level = item.access_level;
- } else if (item.type === LEVEL_TYPES.USER) {
- obj.user_id = item.user_id;
- } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
- obj.deploy_key_id = item.deploy_key_id;
- } else if (item.type === LEVEL_TYPES.GROUP) {
- obj.group_id = item.group_id;
- }
-
- return obj;
- });
-
- return accessLevels;
- }
-
- addSelectedItem(selectedItem) {
- let itemToAdd = {};
-
- let index = -1;
- let alreadyAdded = false;
- const selectedItems = this.getAllSelectedItems();
-
- // Compare IDs based on selectedItem.type
- selectedItems.forEach((item, i) => {
- let comparator;
- switch (selectedItem.type) {
- case LEVEL_TYPES.ROLE:
- comparator = LEVEL_ID_PROP.ROLE;
- // If the item already exists, just use it
- if (item[comparator] === selectedItem.id) {
- alreadyAdded = true;
- }
- break;
- case LEVEL_TYPES.GROUP:
- comparator = LEVEL_ID_PROP.GROUP;
- break;
- case LEVEL_TYPES.DEPLOY_KEY:
- comparator = LEVEL_ID_PROP.DEPLOY_KEY;
- break;
- case LEVEL_TYPES.USER:
- comparator = LEVEL_ID_PROP.USER;
- break;
- default:
- break;
- }
-
- if (selectedItem.id === item[comparator]) {
- index = i;
- }
- });
-
- if (alreadyAdded) {
- return;
- }
-
- if (index !== -1 && selectedItems[index]._destroy) {
- delete selectedItems[index]._destroy;
- return;
- }
-
- itemToAdd.type = selectedItem.type;
-
- if (selectedItem.type === LEVEL_TYPES.USER) {
- itemToAdd = {
- user_id: selectedItem.id,
- name: selectedItem.name || '_name1',
- username: selectedItem.username || '_username1',
- avatar_url: selectedItem.avatar_url || '_avatar_url1',
- type: LEVEL_TYPES.USER,
- };
- } else if (selectedItem.type === LEVEL_TYPES.ROLE) {
- itemToAdd = {
- access_level: selectedItem.id,
- type: LEVEL_TYPES.ROLE,
- };
- } else if (selectedItem.type === LEVEL_TYPES.GROUP) {
- itemToAdd = {
- group_id: selectedItem.id,
- type: LEVEL_TYPES.GROUP,
- };
- } else if (selectedItem.type === LEVEL_TYPES.DEPLOY_KEY) {
- itemToAdd = {
- deploy_key_id: selectedItem.id,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- }
-
- this.items.push(itemToAdd);
- }
-
- removeSelectedItem(itemToDelete) {
- let index = -1;
- const selectedItems = this.getAllSelectedItems();
-
- // To find itemToDelete on selectedItems, first we need the index
- selectedItems.every((item, i) => {
- if (item.type !== itemToDelete.type) {
- return true;
- }
-
- if (
- (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.DEPLOY_KEY && item.deploy_key_id === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id)
- ) {
- index = i;
- }
-
- // Break once we have index set
- return !(index > -1);
- });
-
- // if ItemToDelete is not really selected do nothing
- if (index === -1) {
- return;
- }
-
- if (selectedItems[index].persisted) {
- // If we toggle an item that has been already marked with _destroy
- if (selectedItems[index]._destroy) {
- delete selectedItems[index]._destroy;
- } else {
- selectedItems[index]._destroy = '1';
- }
- } else {
- selectedItems.splice(index, 1);
- }
- }
-
- toggleLabel() {
- const currentItems = this.getSelectedItems();
- const $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
-
- if (currentItems.length === 0) {
- $dropdownToggleText.addClass('is-default');
- return this.defaultLabel;
- }
-
- $dropdownToggleText.removeClass('is-default');
-
- if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
- const roleData = this.accessLevelsData.find(
- (data) => data.id === currentItems[0].access_level,
- );
- return roleData.text;
- }
-
- const labelPieces = [];
- const counts = countBy(currentItems, (item) => item.type);
-
- if (counts[LEVEL_TYPES.ROLE] > 0) {
- labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
- }
-
- if (counts[LEVEL_TYPES.USER] > 0) {
- labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
- }
-
- if (counts[LEVEL_TYPES.DEPLOY_KEY] > 0) {
- labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
- }
-
- if (counts[LEVEL_TYPES.GROUP] > 0) {
- labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
- }
-
- return labelPieces.join(', ');
- }
-
- getData(query, callback) {
- if (this.hasLicense) {
- Promise.all([
- getDeployKeys(query),
- getUsers(query),
- this.groupsData ? Promise.resolve(this.groupsData) : getGroups(),
- ])
- .then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.groupsData = groupsResponse;
- callback(
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
- );
- })
- .catch(() => {
- createAlert({ message: __('Failed to load groups, users and deploy keys.') });
- });
- } else {
- getDeployKeys(query)
- .then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
- }
- }
-
- consolidateData(deployKeysResponse, usersResponse = [], groupsResponse = []) {
- let consolidatedData = [];
-
- // ID property is handled differently locally from the server
- //
- // For Groups
- // In dropdown: `id`
- // For submit: `group_id`
- //
- // For Roles
- // In dropdown: `id`
- // For submit: `access_level`
- //
- // For Users
- // In dropdown: `id`
- // For submit: `user_id`
- //
- // For Deploy Keys
- // In dropdown: `id`
- // For submit: `deploy_key_id`
-
- /*
- * Build roles
- */
- const roles = this.accessLevelsData.map((level) => {
- /* eslint-disable no-param-reassign */
- // This re-assignment is intentional as
- // level.type property is being used in removeSelectedItem()
- // for comparision, and accessLevelsData is provided by
- // gon.create_access_levels which doesn't have `type` included.
- // See this discussion https://gitlab.com/gitlab-org/gitlab/merge_requests/1629#note_31285823
- level.type = LEVEL_TYPES.ROLE;
- return level;
- });
-
- if (roles.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'header', content: s__('AccessDropdown|Roles') }],
- roles,
- );
- }
-
- if (this.hasLicense) {
- const map = [];
- const selectedItems = this.getSelectedItems();
- /*
- * Build groups
- */
- const groups = groupsResponse.map((group) => ({
- ...group,
- type: LEVEL_TYPES.GROUP,
- }));
-
- /*
- * Build users
- */
- const users = selectedItems
- .filter((item) => item.type === LEVEL_TYPES.USER)
- .map((item) => {
- // Save identifiers for easy-checking more later
- map.push(LEVEL_TYPES.USER + item.user_id);
-
- return {
- id: item.user_id,
- name: item.name,
- username: item.username,
- avatar_url: item.avatar_url,
- type: LEVEL_TYPES.USER,
- };
- });
-
- // Has to be checked against server response
- // because the selected item can be in filter results
- if (gon.current_project_id) {
- usersResponse.forEach((response) => {
- // Add is it has not been added
- if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
- const user = { ...response };
- user.type = LEVEL_TYPES.USER;
- users.push(user);
- }
- });
- }
-
- if (groups.length) {
- if (roles.length) {
- consolidatedData = consolidatedData.concat([{ type: 'divider' }]);
- }
-
- consolidatedData = consolidatedData.concat(
- [{ type: 'header', content: s__('AccessDropdown|Groups') }],
- groups,
- );
- }
-
- if (users.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Users') }],
- users,
- );
- }
- }
-
- const deployKeys = deployKeysResponse.map((response) => {
- const {
- id,
- fingerprint,
- fingerprint_sha256: fingerprintSha256,
- title,
- owner: { avatar_url, name, username },
- } = response;
-
- const availableFingerprint = fingerprintSha256 || fingerprint;
- const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`;
-
- return {
- id,
- title: title.concat(' ', shortFingerprint),
- avatar_url,
- fullname: name,
- username,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- });
-
- if (this.accessLevel === ACCESS_LEVELS.PUSH) {
- if (deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
- }
-
- if (this.accessLevel === ACCESS_LEVELS.CREATE && deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
-
- return consolidatedData;
- }
-
- renderRow(item) {
- let criteria = {};
- let groupRowEl;
-
- // Dectect if the current item is already saved so we can add
- // the `is-active` class so the item looks as marked
- switch (item.type) {
- case LEVEL_TYPES.USER:
- criteria = { user_id: item.id };
- break;
- case LEVEL_TYPES.ROLE:
- criteria = { access_level: item.id };
- break;
- case LEVEL_TYPES.DEPLOY_KEY:
- criteria = { deploy_key_id: item.id };
- break;
- case LEVEL_TYPES.GROUP:
- criteria = { group_id: item.id };
- break;
- default:
- break;
- }
-
- const isActive = find(this.getSelectedItems(), criteria) ? 'is-active' : '';
-
- switch (item.type) {
- case LEVEL_TYPES.USER:
- groupRowEl = this.userRowHtml(item, isActive);
- break;
- case LEVEL_TYPES.ROLE:
- groupRowEl = this.roleRowHtml(item, isActive);
- break;
- case LEVEL_TYPES.DEPLOY_KEY:
- groupRowEl =
- this.accessLevel === ACCESS_LEVELS.PUSH || this.accessLevel === ACCESS_LEVELS.CREATE
- ? this.deployKeyRowHtml(item, isActive)
- : '';
-
- break;
- case LEVEL_TYPES.GROUP:
- groupRowEl = this.groupRowHtml(item, isActive);
- break;
- default:
- groupRowEl = '';
- break;
- }
-
- return groupRowEl;
- }
-
- userRowHtml(user, isActive) {
- const isActiveClass = isActive || '';
- const avatarEl = renderAvatar(user, {
- sizeClass: 's32',
- });
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <div class="gl-avatar-labeled">
- ${avatarEl}
- <div>
- <strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
- <span class="gl-avatar-labeled-sublabel dropdown-menu-user-username">@${
- user.username
- }</span>
- </div>
- </div>
- </a>
- </li>
- `;
- }
-
- deployKeyRowHtml(key, isActive) {
- const isActiveClass = isActive || '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <strong>${escape(key.title)}</strong>
- <p>
- ${sprintf(
- __('Owned by %{image_tag}'),
- {
- image_tag: `<img src="${key.avatar_url}" class="avatar avatar-inline s26" width="30">`,
- },
- false,
- )}
- <strong class="dropdown-menu-user-full-name gl-display-inline">${escape(
- key.fullname,
- )}</strong>
- <span class="dropdown-menu-user-username gl-display-inline">${key.username}</span>
- </p>
- </a>
- </li>
- `;
- }
-
- groupRowHtml(group, isActive) {
- const isActiveClass = isActive || '';
- const avatarEl = group.avatar_url
- ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">`
- : '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- ${avatarEl}
- <span class="dropdown-menu-group-groupname">${group.name}</span>
- </a>
- </li>
- `;
- }
-
- roleRowHtml(role, isActive) {
- const isActiveClass = isActive || '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
- ${escape(role.text)}
- </a>
- </li>
- `;
- }
-}
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index df99aac6b9e..b886bf43b57 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -1,7 +1,9 @@
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
-const USERS_PATH = '/-/autocomplete/users.json';
const GROUPS_PATH = '/-/autocomplete/project_groups.json';
+const USERS_PATH = '/-/autocomplete/users.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
@@ -26,10 +28,14 @@ export const getUsers = (query, states) => {
};
export const getGroups = () => {
- return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
- params: {
- project_id: gon.current_project_id,
- },
+ if (gon.current_project_id) {
+ return Api.projectGroups(gon.current_project_id, {
+ with_shared: true,
+ shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
+ });
+ }
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH)).then(({ data }) => {
+ return data;
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index a2e4827cbfa..ca24e948f69 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -12,13 +12,14 @@ import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } fro
import { createAlert } from '~/alert';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
-import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
+import { LEVEL_TYPES, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from '../constants';
export const i18n = {
- selectUsers: s__('ProtectedEnvironment|Select users'),
+ defaultLabel: s__('AccessDropdown|Select'),
rolesSectionHeader: s__('AccessDropdown|Roles'),
groupsSectionHeader: s__('AccessDropdown|Groups'),
usersSectionHeader: s__('AccessDropdown|Users'),
+ noRole: s__('AccessDropdown|No role'),
deployKeysSectionHeader: s__('AccessDropdown|Deploy Keys'),
ownedBy: __('Owned by %{image_tag}'),
};
@@ -51,7 +52,7 @@ export default {
label: {
type: String,
required: false,
- default: i18n.selectUsers,
+ default: i18n.defaultLabel,
},
disabled: {
type: Boolean,
@@ -68,6 +69,31 @@ export default {
required: false,
default: () => [],
},
+ toggleClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ searchEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ testId: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ showUsers: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -96,6 +122,9 @@ export default {
this.deployKeys.length
);
},
+ isAccessesLevelNoneSelected() {
+ return this.selected.role[0].id === ACCESS_LEVEL_NONE;
+ },
toggleLabel() {
const counts = Object.entries(this.selected).reduce((acc, [key, value]) => {
acc[key] = value.length;
@@ -115,7 +144,11 @@ export default {
const labelPieces = [];
if (counts[LEVEL_TYPES.ROLE] > 0) {
- labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ if (this.isAccessesLevelNoneSelected) {
+ labelPieces.push(this.$options.i18n.noRole);
+ } else {
+ labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ }
}
if (counts[LEVEL_TYPES.USER] > 0) {
@@ -132,8 +165,14 @@ export default {
return labelPieces.join(', ') || this.label;
},
- toggleClass() {
- return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
+ fossWithMergeAccess() {
+ return !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
+ },
+ dropdownToggleClass() {
+ return {
+ 'gl-text-gray-500!': this.toggleLabel === this.label,
+ [this.toggleClass]: true,
+ };
},
selection() {
return [
@@ -180,7 +219,7 @@ export default {
);
},
focusInput() {
- this.$refs.search.focusInput();
+ this.$refs.search?.focusInput();
},
getData({ initial = false } = {}) {
this.initialLoading = initial;
@@ -190,10 +229,10 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
+ this.groups.length ? Promise.resolve(this.groups) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse);
this.setSelected({ initial });
})
.catch(() =>
@@ -224,13 +263,18 @@ export default {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
- this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
- id,
- name,
- username,
- avatar_url,
- type: LEVEL_TYPES.USER,
- }));
+
+ // Has to be checked against server response
+ // because the selected item can be in filter results
+ if (this.showUsers) {
+ this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
+ id,
+ name,
+ username,
+ avatar_url,
+ type: LEVEL_TYPES.USER,
+ }));
+ }
}
this.deployKeys = deployKeysResponse.map((response) => {
@@ -328,14 +372,38 @@ export default {
return [...added, ...removed, ...preserved];
},
onItemClick(item) {
- this.toggleSelection(this.selected[item.type], item);
+ this.toggleSelection(item);
this.emitUpdate();
},
- toggleSelection(arr, item) {
- const itemIndex = arr.findIndex(({ id }) => id === item.id);
- if (itemIndex > -1) {
- arr.splice(itemIndex, 1);
- } else arr.push(item);
+ toggleSelection(item) {
+ if (item.id === ACCESS_LEVEL_NONE) {
+ this.selected[LEVEL_TYPES.ROLE] = [item];
+ return;
+ }
+
+ const itemSelected = this.isSelected(item);
+ if (itemSelected) {
+ this.selected[item.type] = this.selected[item.type].filter(({ id }) => id !== item.id);
+ return;
+ }
+
+ // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
+ // remove all preselected items before selecting this item
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
+ if (this.fossWithMergeAccess) this.clearSelection();
+ else if (item.type === LEVEL_TYPES.ROLE) this.unselectNone();
+
+ this.selected[item.type].push(item);
+ },
+ unselectNone() {
+ this.selected[LEVEL_TYPES.ROLE] = this.selected[LEVEL_TYPES.ROLE].filter(
+ ({ id }) => id !== ACCESS_LEVEL_NONE,
+ );
+ },
+ clearSelection() {
+ Object.values(LEVEL_TYPES).forEach((level) => {
+ this.selected[level] = [];
+ });
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);
@@ -346,6 +414,10 @@ export default {
onHide() {
this.$emit('hidden', this.selection);
},
+ onShown() {
+ this.$emit('shown');
+ this.focusInput();
+ },
},
};
</script>
@@ -354,13 +426,15 @@ export default {
<gl-dropdown
:disabled="disabled || initialLoading"
:text="toggleLabel"
- class="gl-min-w-20"
- :toggle-class="toggleClass"
+ :block="block"
+ class="gl-min-w-20 gl-p-0!"
+ :toggle-class="dropdownToggleClass"
aria-labelledby="allowed-users-label"
- @shown="focusInput"
+ :data-testid="testId"
+ @shown="onShown"
@hidden="onHide"
>
- <template #header>
+ <template v-if="searchEnabled" #header>
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="roles.length">
diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js
index 595cbc9c991..37a9fe0c741 100644
--- a/app/assets/javascripts/projects/settings/constants.js
+++ b/app/assets/javascripts/projects/settings/constants.js
@@ -7,13 +7,6 @@ export const LEVEL_TYPES = {
GROUP: 'group',
};
-export const LEVEL_ID_PROP = {
- ROLE: 'access_level',
- USER: 'user_id',
- DEPLOY_KEY: 'deploy_key_id',
- GROUP: 'group_id',
-};
-
export const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 941efaef3bc..67afbee3854 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -7,8 +7,8 @@ export const initAccessDropdown = (el, options) => {
return null;
}
- const { accessLevelsData, accessLevel } = options;
- const { label, disabled, preselectedItems } = el.dataset;
+ const { accessLevelsData, ...props } = options;
+ const { label, disabled, preselectedItems } = el.dataset || {};
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
@@ -18,20 +18,35 @@ export const initAccessDropdown = (el, options) => {
return new Vue({
el,
+ name: 'AccessDropdownRoot',
+ data() {
+ return { preselected };
+ },
+ methods: {
+ setPreselectedItems(items) {
+ this.preselected = items;
+ },
+ },
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
props: {
- accessLevel,
- accessLevelsData: accessLevelsData.roles,
- preselectedItems: preselected,
label,
disabled,
+ accessLevelsData: accessLevelsData.roles,
+ preselectedItems: this.preselected,
+ ...props,
},
on: {
select(selected) {
vm.$emit('select', selected);
},
+ shown() {
+ vm.$emit('shown');
+ },
+ hidden() {
+ vm.$emit('hidden');
+ },
},
});
},
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 4affcd926d4..09bc275cbd4 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
@@ -1,5 +1,14 @@
<script>
-import { GlButton, GlForm, GlFormGroup, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
+import {
+ GlButton,
+ GlForm,
+ GlFormGroup,
+ GlFormInputGroup,
+ GlFormInput,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { isEmptyValue, hasMinimumLength, isIntegerGreaterThan, isEmail } from '~/lib/utils/forms';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
@@ -23,6 +32,9 @@ import {
} from '../custom_email_constants';
export default {
+ customEmailHelpUrl: helpPagePath('user/project/service_desk/configure.html', {
+ anchor: 'custom-email-address',
+ }),
components: {
ClipboardButton,
GlButton,
@@ -30,6 +42,8 @@ export default {
GlFormGroup,
GlFormInputGroup,
GlFormInput,
+ GlLink,
+ GlSprintf,
},
I18N_FORM_INTRODUCTION_PARAGRAPH,
I18N_FORM_CUSTOM_EMAIL_LABEL,
@@ -137,7 +151,19 @@ export default {
<template>
<div>
- <p>{{ $options.I18N_FORM_INTRODUCTION_PARAGRAPH }}</p>
+ <p>
+ <gl-sprintf :message="$options.I18N_FORM_INTRODUCTION_PARAGRAPH">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.customEmailHelpUrl"
+ class="gl-display-inline-block"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
<gl-form class="js-quick-submit" @submit.prevent="onSubmit">
<gl-form-group
:label="$options.I18N_FORM_FORWARDING_LABEL"
@@ -149,7 +175,6 @@ export default {
id="custom-email-form-forwarding"
ref="service-desk-incoming-email"
type="text"
- data-testid="custom-email-form-forwarding"
:aria-label="$options.I18N_FORM_FORWARDING_LABEL"
:value="incomingEmail"
:disabled="true"
@@ -167,7 +192,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_CUSTOM_EMAIL_LABEL"
label-for="custom-email-form-custom-email"
- data-testid="form-group-custom-email"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_CUSTOM_EMAIL"
class="gl-mt-3"
:description="$options.I18N_FORM_CUSTOM_EMAIL_DESCRIPTION"
@@ -191,7 +215,6 @@ export default {
<gl-form-group
:label="$options.I18N_FORM_SMTP_ADDRESS_LABEL"
label-for="custom-email-form-smtp-address"
- data-testid="form-group-smtp-address"
:invalid-feedback="$options.I18N_FORM_INVALID_FEEDBACK_SMTP_ADDRESS"
class="gl-mt-3"
>
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 7e040e6001a..03ba99bcf71 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
@@ -233,6 +233,7 @@ export default {
<gl-link
:href="$options.FEEDBACK_ISSUE_URL"
target="_blank"
+ data-testid="feedback-link"
class="gl-text-blue-600 font-size-inherit"
>{{ content }}
</gl-link>
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 cdf2e53982e..aafd77bd25e 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
@@ -17,7 +17,7 @@ export const I18N_TOAST_ENABLED = s__('ServiceDesk|Custom email enabled.');
export const I18N_TOAST_DISABLED = s__('ServiceDesk|Custom email disabled.');
export const I18N_FORM_INTRODUCTION_PARAGRAPH = s__(
- 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials.',
+ 'ServiceDesk|Connect a custom email address your customers can use to create Service Desk issues. Forward all emails from your custom email address to the Service Desk email address of this project. GitLab will send Service Desk emails from the custom address on your behalf using your SMTP credentials. %{linkStart}Learn more about prerequisites and the verification process%{linkEnd}.',
);
export const I18N_FORM_FORWARDING_LABEL = s__(
'ServiceDesk|Service Desk email address to forward emails to',
diff --git a/app/assets/javascripts/projects/star.js b/app/assets/javascripts/projects/star.js
deleted file mode 100644
index f294811dfff..00000000000
--- a/app/assets/javascripts/projects/star.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { spriteIcon } from '~/lib/utils/common_utils';
-import { __, s__ } from '~/locale';
-
-export default class Star {
- constructor(containerSelector = '.project-home-panel') {
- const container = document.querySelector(containerSelector);
- const starToggle = container.querySelector('.toggle-star');
- starToggle?.addEventListener('click', function toggleStarClickCallback() {
- const starSpan = starToggle.querySelector('span');
- const starIcon = starToggle.querySelector('svg');
- const iconClasses = Array.from(starIcon.classList.values());
-
- axios
- .post(starToggle.dataset.endpoint)
- .then(({ data }) => {
- const isStarred = starSpan.classList.contains('starred');
- starToggle.parentNode.querySelector('.count').textContent = data.star_count;
-
- if (isStarred) {
- starSpan.classList.remove('starred');
- starSpan.textContent = s__('StarProject|Star');
- starIcon.remove();
- // eslint-disable-next-line no-unsanitized/method
- starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star-o', iconClasses));
- } else {
- starSpan.classList.add('starred');
- starSpan.textContent = __('Unstar');
- starIcon.remove();
-
- // eslint-disable-next-line no-unsanitized/method
- starSpan.insertAdjacentHTML('beforebegin', spriteIcon('star', iconClasses));
- }
- })
- .catch(() =>
- createAlert({
- message: __('Star toggle failed. Try again later.'),
- }),
- );
- });
- }
-}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index a11201627a4..49c55efca7b 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -4,16 +4,15 @@ import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __, s__ } from '~/locale';
-import AccessDropdown from '~/projects/settings/access_dropdown';
import { initToggle } from '~/toggles';
import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
+import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import {
BRANCH_RULES_ANCHOR,
PROTECTED_BRANCHES_ANCHOR,
IS_PROTECTED_BRANCH_CREATED,
ACCESS_LEVELS,
- LEVEL_TYPES,
} from './constants';
export default class ProtectedBranchCreate {
@@ -21,14 +20,17 @@ export default class ProtectedBranchCreate {
this.hasLicense = options.hasLicense;
this.$form = $('.js-new-protected-branch');
this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
- this.currentProjectUserDefaults = {};
- this.buildDropdowns();
-
this.forcePushToggle = initToggle(document.querySelector('.js-force-push-toggle'));
-
if (this.hasLicense) {
this.codeOwnerToggle = initToggle(document.querySelector('.js-code-owner-toggle'));
}
+
+ this.selectedItems = {
+ [ACCESS_LEVELS.PUSH]: [],
+ [ACCESS_LEVELS.MERGE]: [],
+ };
+ this.initDropdowns();
+
this.showSuccessAlertIfNeeded();
this.bindEvents();
}
@@ -37,29 +39,26 @@ export default class ProtectedBranchCreate {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
- buildDropdowns() {
- const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge');
- const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push');
-
+ initDropdowns() {
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
- this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
- $dropdown: $allowedToMergeDropdown,
- accessLevelsData: gon.merge_access_levels,
- onSelect: this.onSelectCallback,
+ const allowedToMergeSelector = 'js-allowed-to-merge';
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown({
+ selector: allowedToMergeSelector,
accessLevel: ACCESS_LEVELS.MERGE,
- hasLicense: this.hasLicense,
+ accessLevelsData: gon.merge_access_levels,
+ testId: 'allowed-to-merge-dropdown',
});
// Allowed to Push dropdown
- this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
- $dropdown: $allowedToPushDropdown,
- accessLevelsData: gon.push_access_levels,
- onSelect: this.onSelectCallback,
+ const allowedToPushSelector = 'js-allowed-to-push';
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown({
+ selector: allowedToPushSelector,
accessLevel: ACCESS_LEVELS.PUSH,
- hasLicense: this.hasLicense,
+ accessLevelsData: gon.push_access_levels,
+ testId: 'allowed-to-push-dropdown',
});
this.createItemDropdown = new CreateItemDropdown({
@@ -71,14 +70,40 @@ export default class ProtectedBranchCreate {
});
}
+ buildDropdown({ selector, accessLevel, accessLevelsData, testId }) {
+ const [el] = this.$form.find(`.${selector}`);
+ if (!el) return undefined;
+
+ const projectId = gon.current_project_id;
+ const dropdown = initAccessDropdown(el, {
+ toggleClass: `${selector} gl-form-input-lg`,
+ hasLicense: this.hasLicense,
+ searchEnabled: el.dataset.filter !== undefined,
+ showUsers: projectId !== undefined,
+ block: true,
+ accessLevel,
+ accessLevelsData,
+ testId,
+ });
+
+ dropdown.$on('select', (selected) => {
+ this.selectedItems[accessLevel] = selected;
+ this.onSelectCallback();
+ });
+
+ dropdown.$on('shown', () => {
+ this.createItemDropdown.close();
+ });
+
+ return dropdown;
+ }
+
// Enable submit button after selecting an option
onSelect() {
- const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
- const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const toggle = !(
this.$form.find('input[name="protected_branch[name]"]').val() &&
- $allowedToMerge.length &&
- $allowedToPush.length
+ this.selectedItems[ACCESS_LEVELS.MERGE].length &&
+ this.selectedItems[ACCESS_LEVELS.PUSH].length
);
this.$form.find('button[type="submit"]').attr('disabled', toggle);
@@ -137,32 +162,8 @@ export default class ProtectedBranchCreate {
},
};
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevel = ACCESS_LEVELS[level];
- const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
- const levelAttributes = [];
-
- selectedItems.forEach((item) => {
- if (item.type === LEVEL_TYPES.USER) {
- levelAttributes.push({
- user_id: item.user_id,
- });
- } else if (item.type === LEVEL_TYPES.ROLE) {
- levelAttributes.push({
- access_level: item.access_level,
- });
- } else if (item.type === LEVEL_TYPES.GROUP) {
- levelAttributes.push({
- group_id: item.group_id,
- });
- } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
- levelAttributes.push({
- deploy_key_id: item.deploy_key_id,
- });
- }
- });
-
- formData.protected_branch[`${accessLevel}_attributes`] = levelAttributes;
+ Object.values(ACCESS_LEVELS).forEach((level) => {
+ formData.protected_branch[`${level}_attributes`] = this.selectedItems[level];
});
return formData;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index b6c86750723..29034b3bc0e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -2,28 +2,23 @@ import { find } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import AccessDropdown from '~/projects/settings/access_dropdown';
import { initToggle } from '~/toggles';
+import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
export default class ProtectedBranchEdit {
constructor(options) {
this.hasLicense = options.hasLicense;
- this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
- this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
- this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
- this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(
- `.${ACCESS_LEVELS.MERGE}-container`,
- );
- this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(
- `.${ACCESS_LEVELS.PUSH}-container`,
- );
+ this.selectedItems = {
+ [ACCESS_LEVELS.PUSH]: [],
+ [ACCESS_LEVELS.MERGE]: [],
+ };
+ this.initDropdowns();
- this.buildDropdowns();
this.initToggles();
}
@@ -67,90 +62,96 @@ export default class ProtectedBranchEdit {
}
}
- updateProtectedBranch(formData, callback) {
- axios
- .patch(this.$wrap.data('url'), {
- protected_branch: formData,
- })
- .then(callback)
- .catch(() => {
- createAlert({ message: __('Failed to update branch!') });
- });
+ initDropdowns() {
+ // Allowed to Merge dropdown
+ this[`${ACCESS_LEVELS.MERGE}_dropdown`] = this.buildDropdown(
+ 'js-allowed-to-merge',
+ ACCESS_LEVELS.MERGE,
+ gon.merge_access_levels,
+ 'protected-branch-allowed-to-merge',
+ );
+
+ // Allowed to Push dropdown
+ this[`${ACCESS_LEVELS.PUSH}_dropdown`] = this.buildDropdown(
+ 'js-allowed-to-push',
+ ACCESS_LEVELS.PUSH,
+ gon.push_access_levels,
+ 'protected-branch-allowed-to-push',
+ );
}
- buildDropdowns() {
- // Allowed to merge dropdown
- this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
- accessLevel: ACCESS_LEVELS.MERGE,
- accessLevelsData: gon.merge_access_levels,
- $dropdown: this.$allowedToMergeDropdown,
- onSelect: this.onSelectOption.bind(this),
- onHide: this.onDropdownHide.bind(this),
+ buildDropdown(selector, accessLevel, accessLevelsData, testId) {
+ const [el] = this.$wrap.find(`.${selector}`);
+ if (!el) return undefined;
+
+ const projectId = gon.current_project_id;
+ const dropdown = initAccessDropdown(el, {
+ toggleClass: selector,
hasLicense: this.hasLicense,
+ searchEnabled: el.dataset.filter !== undefined,
+ showUsers: projectId !== undefined,
+ block: true,
+ accessLevel,
+ accessLevelsData,
+ testId,
});
- // Allowed to push dropdown
- this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
- accessLevel: ACCESS_LEVELS.PUSH,
- accessLevelsData: gon.push_access_levels,
- $dropdown: this.$allowedToPushDropdown,
- onSelect: this.onSelectOption.bind(this),
- onHide: this.onDropdownHide.bind(this),
- hasLicense: this.hasLicense,
+ dropdown.$on('select', (selected) => this.onSelectItems(accessLevel, selected));
+ dropdown.$on('hidden', () => this.onDropdownHide());
+
+ this.initSelectedItems(dropdown, accessLevel);
+ return dropdown;
+ }
+
+ initSelectedItems(dropdown, accessLevel) {
+ 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 };
+ if (item.type === LEVEL_TYPES.GROUP) return { id: item.id, group_id: item.group_id };
+ return { id: item.id, deploy_key_id: item.deploy_key_id };
});
}
- onSelectOption() {
+ onSelectItems(accessLevel, selected) {
+ this.selectedItems[accessLevel] = selected;
this.hasChanges = true;
}
onDropdownHide() {
- if (!this.hasChanges) {
- return;
- }
-
- this.hasChanges = true;
+ if (!this.hasChanges) return;
this.updatePermissions();
}
- updatePermissions() {
- const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
- const accessLevelName = ACCESS_LEVELS[level];
- const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
- acc[`${accessLevelName}_attributes`] = inputData;
-
- return acc;
- }, {});
-
+ updateProtectedBranch(formData, callback) {
axios
.patch(this.$wrap.data('url'), {
protected_branch: formData,
})
- .then(({ data }) => {
- this.hasChanges = false;
-
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevelName = ACCESS_LEVELS[level];
-
- // The data coming from server will be the new persisted *state* for each dropdown
- this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
- });
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
- })
+ .then(callback)
.catch(() => {
- this.$allowedToMergeDropdown.enable();
- this.$allowedToPushDropdown.enable();
createAlert({ message: __('Failed to update branch!') });
});
}
- setSelectedItemsToDropdown(items = [], dropdownName) {
+ updatePermissions() {
+ const formData = Object.values(ACCESS_LEVELS).reduce((acc, level) => {
+ acc[`${level}_attributes`] = this.selectedItems[level];
+ return acc;
+ }, {});
+ this.updateProtectedBranch(formData, ({ data }) => {
+ this.hasChanges = false;
+ Object.values(ACCESS_LEVELS).forEach((level) => {
+ this.setSelectedItemsToDropdown(data[level], level);
+ });
+ });
+ }
+
+ setSelectedItemsToDropdown(items = [], accessLevel) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
- const selectedItems = this[dropdownName].getSelectedItems();
+ const selectedItems = this.selectedItems[accessLevel];
const currentSelectedItem = find(selectedItems, {
user_id: currentItem.user_id,
});
@@ -164,7 +165,8 @@ export default class ProtectedBranchEdit {
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
- } else if (currentItem.group_id) {
+ }
+ if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
@@ -181,6 +183,7 @@ export default class ProtectedBranchEdit {
};
});
- this[dropdownName].setSelectedItems(itemsToAdd);
+ this.selectedItems[accessLevel] = itemsToAdd;
+ this[`${accessLevel}_dropdown`]?.setPreselectedItems(itemsToAdd);
}
}
diff --git a/app/assets/javascripts/protected_tags/constants.js b/app/assets/javascripts/protected_tags/constants.js
index 758b820c4c4..d332ba1635f 100644
--- a/app/assets/javascripts/protected_tags/constants.js
+++ b/app/assets/javascripts/protected_tags/constants.js
@@ -1,7 +1,3 @@
-import { s__ } from '~/locale';
-
-export const FAILED_TO_UPDATE_TAG_MESSAGE = s__('ProjectSettings|Failed to update tag!');
-
export const ACCESS_LEVELS = {
CREATE: 'create_access_levels',
};
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index 365b9a3b142..b5661af352c 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -3,8 +3,8 @@ import CreateItemDropdown from '~/create_item_dropdown';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { s__, __ } from '~/locale';
-import AccessDropdown from '~/projects/settings/access_dropdown';
-import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+import { initAccessDropdown } from '~/projects/settings/init_access_dropdown';
+import { ACCESS_LEVELS } from './constants';
export default class ProtectedTagCreate {
constructor({ hasLicense }) {
@@ -12,6 +12,7 @@ export default class ProtectedTagCreate {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
this.bindEvents();
+ this.selectedItems = [];
}
bindEvents() {
@@ -19,20 +20,9 @@ export default class ProtectedTagCreate {
}
buildDropdowns() {
- const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
-
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
- // Allowed to Create dropdown
- this.protectedTagAccessDropdown = new AccessDropdown({
- $dropdown: $allowedToCreateDropdown,
- accessLevelsData: gon.create_access_levels,
- onSelect: this.onSelectCallback,
- accessLevel: ACCESS_LEVELS.CREATE,
- hasLicense: this.hasLicense,
- });
-
// Protected tag dropdown
this.createItemDropdown = new CreateItemDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
@@ -41,17 +31,36 @@ export default class ProtectedTagCreate {
onSelect: this.onSelectCallback,
getData: ProtectedTagCreate.getProtectedTags,
});
+
+ // Allowed to Create dropdown
+ const createTagSelector = 'js-allowed-to-create';
+ const [dropdownEl] = this.$form.find(`.${createTagSelector}`);
+ this.protectedTagAccessDropdown = initAccessDropdown(dropdownEl, {
+ toggleClass: createTagSelector,
+ hasLicense: this.hasLicense,
+ accessLevel: ACCESS_LEVELS.CREATE,
+ accessLevelsData: gon.create_access_levels,
+ searchEnabled: dropdownEl.dataset.filter !== undefined,
+ testId: 'allowed_to_create_dropdown',
+ });
+
+ this.protectedTagAccessDropdown.$on('select', (selected) => {
+ this.selectedItems = selected;
+ this.onSelectCallback();
+ });
+
+ this.protectedTagAccessDropdown.$on('shown', () => {
+ this.createItemDropdown.close();
+ });
}
// This will run after clicked callback
onSelect() {
// Enable submit button
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
- const $allowedToCreateInput = this.protectedTagAccessDropdown.getSelectedItems();
-
this.$form
.find('button[type="submit"]')
- .prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
+ .prop('disabled', !($tagInput.val() && this.selectedItems.length));
}
static getProtectedTags(term, callback) {
@@ -65,35 +74,7 @@ export default class ProtectedTagCreate {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
-
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevel = ACCESS_LEVELS[level];
- const selectedItems = this.protectedTagAccessDropdown.getSelectedItems();
- const levelAttributes = [];
-
- selectedItems.forEach((item) => {
- if (item.type === LEVEL_TYPES.USER) {
- levelAttributes.push({
- user_id: item.user_id,
- });
- } else if (item.type === LEVEL_TYPES.ROLE) {
- levelAttributes.push({
- access_level: item.access_level,
- });
- } else if (item.type === LEVEL_TYPES.GROUP) {
- levelAttributes.push({
- group_id: item.group_id,
- });
- } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
- levelAttributes.push({
- deploy_key_id: item.deploy_key_id,
- });
- }
- });
-
- formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes;
- });
-
+ formData.protected_tag[`${ACCESS_LEVELS.CREATE}_attributes`] = this.selectedItems;
return formData;
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
deleted file mode 100644
index 4fa3ac3be4b..00000000000
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import { find } from 'lodash';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import AccessDropdown from '~/projects/settings/access_dropdown';
-import { ACCESS_LEVELS, LEVEL_TYPES, FAILED_TO_UPDATE_TAG_MESSAGE } from './constants';
-
-export default class ProtectedTagEdit {
- constructor(options) {
- this.hasLicense = options.hasLicense;
- this.hasChanges = false;
- this.$wrap = options.$wrap;
- this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
-
- this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest(
- '.create_access_levels-container',
- );
-
- this.buildDropdowns();
- }
-
- buildDropdowns() {
- // Allowed to create dropdown
- this.protectedTagAccessDropdown = new AccessDropdown({
- accessLevel: ACCESS_LEVELS.CREATE,
- accessLevelsData: gon.create_access_levels,
- $dropdown: this.$allowedToCreateDropdownButton,
- onSelect: this.onSelectOption.bind(this),
- onHide: this.onDropdownHide.bind(this),
- hasLicense: this.hasLicense,
- });
- }
-
- onSelectOption() {
- this.hasChanges = true;
- }
-
- onDropdownHide() {
- if (!this.hasChanges) {
- return;
- }
-
- this.hasChanges = true;
- this.updatePermissions();
- }
-
- updatePermissions() {
- const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
- const accessLevelName = ACCESS_LEVELS[level];
- const inputData = this.protectedTagAccessDropdown.getInputData(accessLevelName);
- acc[`${accessLevelName}_attributes`] = inputData;
-
- return acc;
- }, {});
-
- axios
- .patch(this.$wrap.data('url'), {
- protected_tag: formData,
- })
- .then(({ data }) => {
- this.hasChanges = false;
-
- Object.keys(ACCESS_LEVELS).forEach((level) => {
- const accessLevelName = ACCESS_LEVELS[level];
-
- // The data coming from server will be the new persisted *state* for each dropdown
- this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`);
- });
- })
- .catch(() => {
- window.scrollTo({ top: 0, behavior: 'smooth' });
- createAlert({
- message: FAILED_TO_UPDATE_TAG_MESSAGE,
- });
- });
- }
-
- setSelectedItemsToDropdown(items = []) {
- const itemsToAdd = items.map((currentItem) => {
- if (currentItem.user_id) {
- // Do this only for users for now
- // get the current data for selected items
- const selectedItems = this.protectedTagAccessDropdown.getSelectedItems();
- const currentSelectedItem = find(selectedItems, {
- user_id: currentItem.user_id,
- });
-
- return {
- id: currentItem.id,
- user_id: currentItem.user_id,
- type: LEVEL_TYPES.USER,
- persisted: true,
- name: currentSelectedItem.name,
- username: currentSelectedItem.username,
- avatar_url: currentSelectedItem.avatar_url,
- };
- } else if (currentItem.group_id) {
- return {
- id: currentItem.id,
- group_id: currentItem.group_id,
- type: LEVEL_TYPES.GROUP,
- persisted: true,
- };
- }
-
- return {
- id: currentItem.id,
- access_level: currentItem.access_level,
- type: LEVEL_TYPES.ROLE,
- persisted: true,
- };
- });
-
- this.protectedTagAccessDropdown.setSelectedItems(itemsToAdd);
- }
-}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
new file mode 100644
index 00000000000..82b2ecc5f5c
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -0,0 +1,113 @@
+<script>
+import AccessDropdown from '~/projects/settings/components/access_dropdown.vue';
+import { createAlert } from '~/alert';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
+
+export const i18n = {
+ failureMessage: s__('ProjectSettings|Failed to update tag!'),
+};
+
+export default {
+ i18n,
+ ACCESS_LEVELS,
+ name: 'ProtectedTagEdit',
+ components: {
+ AccessDropdown,
+ },
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ accessLevelsData: {
+ type: Array,
+ required: true,
+ },
+ hasLicense: {
+ required: false,
+ type: Boolean,
+ default: true,
+ },
+ preselectedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ searchEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ data() {
+ return {
+ selected: this.preselectedItems,
+ };
+ },
+ methods: {
+ hasChanges(permissions) {
+ return permissions.some(({ id, _destroy }) => id === undefined || _destroy);
+ },
+ updatePermissions(permissions) {
+ if (!this.hasChanges(permissions)) return;
+ axios
+ .patch(this.url, {
+ protected_tag: {
+ [`${ACCESS_LEVELS.CREATE}_attributes`]: permissions,
+ },
+ })
+ .then(this.setSelected)
+ .catch(() => {
+ createAlert({
+ message: i18n.failureMessage,
+ parent: this.parentContainer,
+ });
+ });
+ },
+ setSelected({ data }) {
+ if (!data) return;
+ this.selected = data[ACCESS_LEVELS.CREATE].map(
+ ({ id, user_id: userId, group_id: groupId, access_level: accessLevel }) => {
+ if (userId) {
+ return {
+ id,
+ user_id: userId,
+ type: LEVEL_TYPES.USER,
+ };
+ }
+
+ if (groupId) {
+ return {
+ id,
+ group_id: groupId,
+ type: LEVEL_TYPES.GROUP,
+ };
+ }
+
+ return {
+ id,
+ access_level: accessLevel,
+ type: LEVEL_TYPES.ROLE,
+ };
+ },
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <access-dropdown
+ toggle-class="js-allowed-to-create gl-max-w-34"
+ test-id="allowed_to_create_dropdown"
+ :has-license="hasLicense"
+ :access-level="$options.ACCESS_LEVELS.CREATE"
+ :access-levels-data="accessLevelsData"
+ :preselected-items="selected"
+ :search-enabled="searchEnabled"
+ :block="true"
+ @hidden="updatePermissions"
+ />
+</template>
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 8ceb970bf03..444d6e9cf76 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -1,21 +1,49 @@
-/* eslint-disable no-new */
-
-import $ from 'jquery';
-import ProtectedTagEdit from './protected_tag_edit';
+import Vue from 'vue';
+import * as Sentry from '@sentry/browser';
+import ProtectedTagEdit from './protected_tag_edit.vue';
export default class ProtectedTagEditList {
constructor(options) {
this.hasLicense = options.hasLicense;
- this.$wrap = $('.protected-tags-list');
this.initEditForm();
}
initEditForm() {
- this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
- new ProtectedTagEdit({
- $wrap: $(el),
- hasLicense: this.hasLicense,
+ document
+ .querySelector('.protected-tags-list')
+ .querySelectorAll('.js-protected-tag-edit-form')
+ ?.forEach((el) => {
+ const accessDropdownEl = el.querySelector('.js-allowed-to-create');
+ this.initAccessDropdown(accessDropdownEl, {
+ url: el.dataset.url,
+ hasLicense: this.hasLicense,
+ accessLevelsData: gon.create_access_levels.roles,
+ });
});
+ }
+
+ // eslint-disable-next-line class-methods-use-this
+ initAccessDropdown(el, options) {
+ if (!el) return null;
+
+ let preselected = [];
+ try {
+ preselected = JSON.parse(el.dataset.preselectedItems);
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(ProtectedTagEdit, {
+ props: {
+ preselectedItems: preselected,
+ searchEnabled: el.dataset.filter !== undefined,
+ ...options,
+ },
+ });
+ },
});
}
}
diff --git a/app/assets/javascripts/related_issues/components/add_issuable_form.vue b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
index b3033ddf3b6..d36b29f69a5 100644
--- a/app/assets/javascripts/related_issues/components/add_issuable_form.vue
+++ b/app/assets/javascripts/related_issues/components/add_issuable_form.vue
@@ -115,7 +115,8 @@ export default {
addRelatedErrorMessage() {
if (this.itemAddFailureMessage) {
return this.itemAddFailureMessage;
- } else if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
+ }
+ if (this.itemAddFailureType === itemAddFailureTypesMap.NOT_FOUND) {
return addRelatedIssueErrorMap[this.issuableType];
}
// Only other failure is MAX_NUMBER_OF_CHILD_EPICS at the moment
diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js
index 23620432feb..cc0dae355b6 100644
--- a/app/assets/javascripts/related_issues/index.js
+++ b/app/assets/javascripts/related_issues/index.js
@@ -1,10 +1,9 @@
import Vue from 'vue';
-import { TYPE_ISSUE } from '~/issues/constants';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import { parseBoolean } from '~/lib/utils/common_utils';
import RelatedIssuesRoot from './components/related_issues_root.vue';
-export function initRelatedIssues(issueType = TYPE_ISSUE) {
+export function initRelatedIssues() {
const el = document.querySelector('.js-related-issues-root');
if (!el) {
@@ -28,7 +27,7 @@ export function initRelatedIssues(issueType = TYPE_ISSUE) {
canAdmin: parseBoolean(el.dataset.canAddRelatedIssues),
helpPath: el.dataset.helpPath,
showCategorizedIssues: parseBoolean(el.dataset.showCategorizedIssues),
- issuableType: issueType,
+ issuableType: el.dataset.issuableType,
autoCompleteEpics: false,
},
}),
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 6f9f0a81dfd..6565c84fa11 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -7,11 +7,9 @@ import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constant
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { redirectTo, getLocationHash } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import CodeIntelligence from '~/code_navigation/components/app.vue';
import LineHighlighter from '~/blob/line_highlighter';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
@@ -19,15 +17,7 @@ import { addBlameLink } from '~/blob/blob_blame_link';
import highlightMixin from '~/repository/mixins/highlight_mixin';
import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
-import userInfoQuery from '../queries/user_info.query.graphql';
-import applicationInfoQuery from '../queries/application_info.query.graphql';
-import {
- DEFAULT_BLOB_INFO,
- TEXT_FILE_TYPE,
- LFS_STORAGE,
- LEGACY_FILE_TYPES,
- CODEOWNERS_FILE_NAME,
-} from '../constants';
+import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers';
@@ -38,10 +28,8 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
- CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
GlButton,
ForkSuggestion,
- WebIdeLink,
CodeIntelligence,
AiGenie: () => import('ee_component/ai/components/ai_genie.vue'),
},
@@ -68,18 +56,6 @@ export default {
this.userPermissions = project.userPermissions;
},
},
- gitpodEnabled: {
- query: applicationInfoQuery,
- error() {
- this.displayError();
- },
- },
- currentUser: {
- query: userInfoQuery,
- error() {
- this.displayError();
- },
- },
project: {
query: blobInfoQuery,
variables() {
@@ -94,32 +70,17 @@ export default {
return queryVariables;
},
result({ data }) {
- const blob = data.project?.repository?.blobs?.nodes[0] || {};
- this.initHighlightWorker(blob);
+ const repository = data.project?.repository || {};
+ this.blobInfo = repository.blobs?.nodes[0] || {};
+ this.isEmptyRepository = repository.empty;
+ this.projectId = data.project?.id;
- const urlHash = getLocationHash();
- const plain = this.$route?.query?.plain;
+ const usePlain = this.$route?.query?.plain === '1'; // When the 'plain' URL param is present, its value determines which viewer to render
+ const urlHash = getLocationHash(); // If there is a code line hash in the URL we render with the simple viewer
+ const useSimpleViewer = usePlain || urlHash?.startsWith('L') || !this.hasRichViewer;
- // When the 'plain' URL param is present, its value determines which viewer to render:
- // - when 0 and the rich viewer is available we render with it
- // - otherwise we render the simple viewer
- if (plain !== undefined) {
- if (plain === '0' && this.hasRichViewer) {
- this.switchViewer(RICH_BLOB_VIEWER);
- } else {
- this.switchViewer(SIMPLE_BLOB_VIEWER);
- }
- return;
- }
-
- // If there is a code line hash in the URL we render with the simple viewer
- if (urlHash && urlHash.startsWith('L')) {
- this.switchViewer(SIMPLE_BLOB_VIEWER);
- return;
- }
-
- // By default, if present, use the rich viewer to render
- this.switchViewer(this.hasRichViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER);
+ this.initHighlightWorker(this.blobInfo);
+ this.switchViewer(useSimpleViewer ? SIMPLE_BLOB_VIEWER : RICH_BLOB_VIEWER); // By default, if present, use the rich viewer to render
},
error() {
this.displayError();
@@ -127,9 +88,7 @@ export default {
},
},
provide() {
- return {
- blobHash: uniqueId(),
- };
+ return { blobHash: uniqueId() };
},
props: {
path: {
@@ -156,11 +115,13 @@ export default {
isRenderingLegacyTextViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: DEFAULT_BLOB_INFO.project,
- gitpodEnabled: DEFAULT_BLOB_INFO.gitpodEnabled,
currentUser: DEFAULT_BLOB_INFO.currentUser,
useFallback: false,
pathLocks: DEFAULT_BLOB_INFO.pathLocks,
userPermissions: DEFAULT_BLOB_INFO.userPermissions,
+ blobInfo: {},
+ isEmptyRepository: false,
+ projectId: null,
};
},
computed: {
@@ -173,17 +134,9 @@ export default {
isBinaryFileType() {
return this.isBinary || this.blobInfo.simpleViewer?.fileType !== TEXT_FILE_TYPE;
},
- blobInfo() {
- const nodes = this.project?.repository?.blobs?.nodes || [];
-
- return nodes[0] || {};
- },
currentRef() {
return this.originalBranch || this.ref;
},
- isCodeownersFile() {
- return this.path.includes(CODEOWNERS_FILE_NAME);
- },
viewer() {
const { richViewer, simpleViewer } = this.blobInfo;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
@@ -249,22 +202,14 @@ export default {
isUsingLfs() {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
- projectIdAsNumber() {
- return getIdFromGraphQLId(this.project?.id);
- },
},
watch: {
// Watch the URL 'plain' query value to know if the viewer needs changing.
- // This is the case when the user switches the viewer and then goes back
- // through the hystory.
+ // This is the case when the user switches the viewer and then goes back through the history
'$route.query.plain': {
handler(plainValue) {
- this.switchViewer(
- this.hasRichViewer && (plainValue === undefined || plainValue === '0')
- ? RICH_BLOB_VIEWER
- : SIMPLE_BLOB_VIEWER,
- plainValue !== undefined,
- );
+ const useSimpleViewer = plainValue === '1' || !this.hasRichViewer;
+ this.switchViewer(useSimpleViewer ? SIMPLE_BLOB_VIEWER : RICH_BLOB_VIEWER);
},
},
},
@@ -323,21 +268,11 @@ export default {
this.loadLegacyViewer();
}
},
- updateRouteQuery() {
- const plain = this.activeViewerType === SIMPLE_BLOB_VIEWER ? '1' : '0';
-
- if (this.$route?.query?.plain === plain) {
- return;
- }
-
- this.$router.push({
- path: this.$route.path,
- query: { ...this.$route.query, plain },
- });
- },
handleViewerChanged(newViewer) {
this.switchViewer(newViewer);
- this.updateRouteQuery();
+ const plain = newViewer === SIMPLE_BLOB_VIEWER ? '1' : '0';
+ if (this.$route?.query?.plain === plain) return;
+ this.$router.push({ path: this.$route.path, query: { ...this.$route.query, plain } });
},
editBlob(target) {
if (this.showForkSuggestion) {
@@ -370,31 +305,15 @@ export default {
:has-render-error="hasRenderError"
:show-path="false"
:override-copy="true"
+ :show-fork-suggestion="showForkSuggestion"
+ :project-path="projectPath"
+ :project-id="projectId"
@viewer-changed="handleViewerChanged"
@copy="onCopy"
+ @edit="editBlob"
+ @error="displayError"
>
<template #actions>
- <web-ide-link
- v-if="!blobInfo.archived"
- :show-edit-button="!isBinaryFileType"
- class="gl-mr-3"
- :edit-url="blobInfo.editBlobPath"
- :web-ide-url="blobInfo.ideEditPath"
- :needs-to-fork="showForkSuggestion"
- :show-pipeline-editor-button="Boolean(blobInfo.pipelineEditorPath)"
- :pipeline-editor-url="blobInfo.pipelineEditorPath"
- :gitpod-url="blobInfo.gitpodBlobUrl"
- :show-gitpod-button="gitpodEnabled"
- :gitpod-enabled="currentUser && currentUser.gitpodEnabled"
- :project-path="projectPath"
- :project-id="projectIdAsNumber"
- :user-preferences-gitpod-path="currentUser && currentUser.preferencesGitpodPath"
- :user-profile-enable-gitpod-path="currentUser && currentUser.profileEnableGitpodPath"
- is-blob
- disable-fork-modal
- @edit="editBlob"
- />
-
<blob-button-group
v-if="isLoggedIn && !blobInfo.archived"
:path="path"
@@ -403,7 +322,7 @@ export default {
:delete-path="blobInfo.webPath"
:can-push-code="userPermissions.pushCode"
:can-push-to-branch="blobInfo.canCurrentUserPushToBranch"
- :empty-repo="project.repository.empty"
+ :empty-repo="isEmptyRepository"
:project-path="projectPath"
:is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
@@ -418,12 +337,6 @@ export default {
:fork-path="forkPath"
@cancel="setForkTarget(null)"
/>
- <codeowners-validation
- v-if="isCodeownersFile"
- :current-ref="currentRef"
- :project-path="projectPath"
- :file-path="path"
- />
<blob-content
v-if="!blobViewer"
class="js-syntax-highlight"
@@ -441,6 +354,8 @@ export default {
v-else
:blob="blobInfo"
:chunks="chunks"
+ :project-path="projectPath"
+ :current-ref="currentRef"
class="blob-viewer"
@error="onError"
/>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index fa51ef30546..12edeeb0d2f 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -20,13 +20,13 @@ export default {
UserAvatarLink,
TimeagoTooltip,
ClipboardButton,
- CiIcon,
GlButton,
GlButtonGroup,
GlLink,
GlLoadingIcon,
UserAvatarImage,
SignatureBadge,
+ CiBadgeLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -191,18 +191,14 @@ export default {
>
<signature-badge v-if="commit.signature" :signature="commit.signature" />
<div v-if="commit.pipeline" class="ci-status-link">
- <gl-link
- v-gl-tooltip.left
- :href="commit.pipeline.detailedStatus.detailsPath"
- :title="statusTitle"
+ <ci-badge-link
+ :status="commit.pipeline.detailedStatus"
+ :details-path="commit.pipeline.detailedStatus.detailsPath"
+ :aria-label="statusTitle"
+ size="lg"
+ :show-text="false"
class="js-commit-pipeline"
- >
- <ci-icon
- :status="commit.pipeline.detailedStatus"
- :size="24"
- :aria-label="statusTitle"
- />
- </gl-link>
+ />
</div>
<gl-button-group class="gl-ml-4 js-commit-sha-group">
<gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index c839d7a53cd..a76d822317a 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -123,7 +123,8 @@ export default {
path: joinPaths('/-/blob', this.escapedRef, this.path),
refType: this.refType,
});
- } else if (this.isFolder) {
+ }
+ if (this.isFolder) {
return buildURLwithRefType({
path: joinPaths('/-/tree', this.escapedRef, this.path),
refType: this.refType,
@@ -260,7 +261,9 @@ export default {
</gl-intersection-observer>
</td>
<td class="tree-time-ago text-right cursor-default gl-text-secondary">
- <timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
+ <gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
+ <timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
+ </gl-intersection-observer>
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</td>
</tr>
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 3079ef0bfbb..c4d03120c2e 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -27,12 +27,6 @@ export const PDF_MAX_PAGE_LIMIT = 50;
export const ROW_APPEAR_DELAY = 150;
export const DEFAULT_BLOB_INFO = {
- gitpodEnabled: false,
- currentUser: {
- gitpodEnabled: false,
- preferencesGitpodPath: null,
- profileEnableGitpodPath: null,
- },
userPermissions: {
pushCode: false,
downloadCode: false,
@@ -116,5 +110,3 @@ export const POLLING_INTERVAL_BACKOFF = 2;
export const CONFLICTS_MODAL_ID = 'fork-sync-conflicts-modal';
export const FORK_UPDATED_EVENT = 'fork:updated';
-
-export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 87996d0bb85..2b9ec7b63d7 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -8,6 +8,7 @@ export * from './api/tags_api';
export * from './api/alert_management_alerts_api';
export * from './api/harbor_registry';
export * from './api/environments_api';
+export * from './api/application_settings_api';
// Note: It's not possible to spy on methods imported from this file in
// Jest tests.
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 58e4553d00d..bc1f32930e7 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -64,15 +64,12 @@ Sidebar.prototype.addEventListeners = function () {
};
Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
- const $this = $(this);
-
- if ($this.hasClass('right-sidebar-merge-requests')) return;
-
+ const $toggleButtons = $('.js-sidebar-toggle');
const $collapseIcon = $('.js-sidebar-collapse');
const $expandIcon = $('.js-sidebar-expand');
const $toggleContainer = $('.js-sidebar-toggle-container');
const isExpanded = $toggleContainer.data('is-expanded');
- const tooltipLabel = isExpanded ? __('Collapse sidebar') : __('Expand sidebar');
+ const tooltipLabel = isExpanded ? __('Expand sidebar') : __('Collapse sidebar');
e.preventDefault();
if (isExpanded) {
@@ -93,10 +90,10 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
$('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
- $this.attr('data-original-title', tooltipLabel);
- $this.attr('title', tooltipLabel);
- fixTitle($this);
- hide($this);
+ $toggleButtons.attr('data-original-title', tooltipLabel);
+ $toggleButtons.attr('title', tooltipLabel);
+ fixTitle($toggleButtons);
+ hide($toggleButtons);
if (!triggered) {
setCookie('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
diff --git a/app/assets/javascripts/run_modules.js b/app/assets/javascripts/run_modules.js
new file mode 100644
index 00000000000..fabdff9bb76
--- /dev/null
+++ b/app/assets/javascripts/run_modules.js
@@ -0,0 +1,9 @@
+export const runModules = (modules, prefix) => {
+ document
+ .querySelector('meta[name="controller-path"]')
+ .content.split('/')
+ .forEach((part, index, arr) => {
+ const path = `${prefix}${[...arr.slice(0, index), part].join('/')}/index.js`;
+ modules[path]?.();
+ });
+};
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index f5684cebbf9..f83130213f2 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -10,12 +10,13 @@ import { initBlobRefSwitcher } from './under_topbar';
export const initSearchApp = () => {
syntaxHighlight(document.querySelectorAll('.js-search-results'));
const query = queryToObject(window.location.search, { gatherArrays: true });
- const { navigationJsonParsed: navigation } = sidebarInitState() || {};
+ const { navigationJsonParsed: navigation, searchType } = sidebarInitState() || {};
const store = createStore({
query,
navigation,
useSidebarNavigation: gon.use_new_navigation,
+ searchType,
});
initTopbar(store);
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 9962f711892..532a66affd8 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -3,29 +3,46 @@
import { mapState, mapGetters } from 'vuex';
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 { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS, SCOPE_BLOB, SCOPE_PROJECTS } from '../constants';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import {
+ SCOPE_ISSUES,
+ SCOPE_MERGE_REQUESTS,
+ SCOPE_BLOB,
+ SCOPE_PROJECTS,
+ SCOPE_NOTES,
+ SCOPE_COMMITS,
+ SEARCH_TYPE_ADVANCED,
+} from '../constants';
import IssuesFilters from './issues_filters.vue';
import MergeRequestsFilters from './merge_requests_filters.vue';
import BlobsFilters from './blobs_filters.vue';
import ProjectsFilters from './projects_filters.vue';
+import NotesFilters from './notes_filters.vue';
+import CommitsFilters from './commits_filters.vue';
export default {
name: 'GlobalSearchSidebar',
components: {
IssuesFilters,
- ScopeLegacyNavigation,
- ScopeSidebarNavigation,
- SidebarPortal,
MergeRequestsFilters,
BlobsFilters,
ProjectsFilters,
+ NotesFilters,
+ ScopeLegacyNavigation,
+ ScopeSidebarNavigation,
+ SidebarPortal,
+ DomElementListener,
+ SmallScreenDrawerNavigation,
+ CommitsFilters,
},
mixins: [glFeatureFlagsMixin()],
computed: {
// useSidebarNavigation refers to whether the new left sidebar navigation is enabled
- ...mapState(['useSidebarNavigation']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
...mapGetters(['currentScope']),
showIssuesFilters() {
return this.currentScope === SCOPE_ISSUES;
@@ -34,11 +51,25 @@ export default {
return this.currentScope === SCOPE_MERGE_REQUESTS;
},
showBlobFilters() {
- return this.currentScope === SCOPE_BLOB;
+ return this.currentScope === SCOPE_BLOB && this.searchType === SEARCH_TYPE_ADVANCED;
},
showProjectsFilters() {
- // for now the feature flag is here. Since we have only one filter in projects scope
- return this.currentScope === SCOPE_PROJECTS && this.glFeatures.searchProjectsHideArchived;
+ return this.currentScope === SCOPE_PROJECTS;
+ },
+ showNotesFilters() {
+ return (
+ this.currentScope === SCOPE_NOTES &&
+ this.searchType === SEARCH_TYPE_ADVANCED &&
+ this.glFeatures.searchNotesHideArchivedProjects
+ );
+ },
+ showCommitsFilters() {
+ // for now, the feature flag is placed here. Since we have only one filter in commits scope
+ return (
+ this.currentScope === SCOPE_COMMITS &&
+ this.searchType === SEARCH_TYPE_ADVANCED &&
+ this.glFeatures.searchCommitsHideArchivedProjects
+ );
},
showScopeNavigation() {
// showScopeNavigation refers to whether the scope navigation should be shown
@@ -47,27 +78,49 @@ export default {
return Boolean(this.currentScope);
},
},
+ methods: {
+ toggleFiltersFromSidebar() {
+ toggleSuperSidebarCollapsed();
+ },
+ },
};
</script>
<template>
<section v-if="useSidebarNavigation">
+ <dom-element-listener selector="#js-open-mobile-filters" @click="toggleFiltersFromSidebar" />
<sidebar-portal>
<scope-sidebar-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
<blobs-filters v-if="showBlobFilters" />
<projects-filters v-if="showProjectsFilters" />
+ <notes-filters v-if="showNotesFilters" />
+ <commits-filters v-if="showCommitsFilters" />
</sidebar-portal>
</section>
+
<section
v-else-if="showScopeNavigation"
- class="search-sidebar gl-display-flex gl-flex-direction-column gl-md-mr-5 gl-mb-6 gl-mt-5"
+ class="gl-display-flex gl-flex-direction-column gl-lg-mr-0 gl-md-mr-5 gl-lg-mb-6 gl-lg-mt-5"
>
- <scope-legacy-navigation />
- <issues-filters v-if="showIssuesFilters" />
- <merge-requests-filters v-if="showMergeRequestFilters" />
- <blobs-filters v-if="showBlobFilters" />
- <projects-filters v-if="showProjectsFilters" />
+ <div class="search-sidebar gl-display-none gl-lg-display-block">
+ <scope-legacy-navigation />
+ <issues-filters v-if="showIssuesFilters" />
+ <merge-requests-filters v-if="showMergeRequestFilters" />
+ <blobs-filters v-if="showBlobFilters" />
+ <projects-filters v-if="showProjectsFilters" />
+ <notes-filters v-if="showNotesFilters" />
+ <commits-filters v-if="showCommitsFilters" />
+ </div>
+ <small-screen-drawer-navigation class="gl-lg-display-none">
+ <scope-legacy-navigation />
+ <issues-filters v-if="showIssuesFilters" />
+ <merge-requests-filters v-if="showMergeRequestFilters" />
+ <blobs-filters v-if="showBlobFilters" />
+ <projects-filters v-if="showProjectsFilters" />
+ <notes-filters v-if="showNotesFilters" />
+ <commits-filters v-if="showCommitsFilters" />
+ </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 77efbdd9e60..5cddf5e744f 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/data.js
@@ -7,6 +7,11 @@ export const TRACKING_LABEL_CHECKBOX = 'checkbox';
const scopes = {
PROJECTS: 'projects',
+ ISSUES: 'issues',
+ MERGE_REQUESTS: 'merge_requests',
+ NOTES: 'notes',
+ BLOBS: 'blobs',
+ COMMITS: 'commits',
};
const filterParam = 'include_archived';
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
index 1984e3a36c4..c31c46f2e6a 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
@@ -14,7 +14,7 @@ export default {
GlFormCheckbox,
},
computed: {
- ...mapState(['urlQuery']),
+ ...mapState(['urlQuery', 'useSidebarNavigation']),
selectedFilter: {
get() {
return [parseBoolean(this.urlQuery?.include_archived)];
@@ -41,7 +41,9 @@ export default {
<template>
<gl-form-checkbox-group v-model="selectedFilter">
- <h5>{{ $options.archivedFilterData.headerLabel }}</h5>
+ <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ {{ $options.archivedFilterData.headerLabel }}
+ </h5>
<gl-form-checkbox
class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full"
:class="$options.LABEL_DEFAULT_CLASSES"
diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
index 5f4d6fbd56c..ac36ae6b366 100644
--- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
@@ -1,5 +1,10 @@
<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 LanguageFilter from './language_filter/index.vue';
+import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
export default {
@@ -7,6 +12,21 @@ export default {
components: {
LanguageFilter,
FiltersTemplate,
+ ArchivedFilter,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapGetters(['currentScope']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
+ showArchivedFilter() {
+ return this.glFeatures.searchBlobsHideArchivedProjects;
+ },
+ showDivider() {
+ return !this.useSidebarNavigation && this.showArchivedFilter;
+ },
+ hrClasses() {
+ return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
+ },
},
};
</script>
@@ -14,5 +34,7 @@ export default {
<template>
<filters-template>
<language-filter class="gl-mb-5" />
+ <hr v-if="showDivider" :class="hrClasses" />
+ <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/commits_filters.vue b/app/assets/javascripts/search/sidebar/components/commits_filters.vue
new file mode 100644
index 00000000000..4f9fdbe9551
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/commits_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'CommitsFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/filters_template.vue b/app/assets/javascripts/search/sidebar/components/filters_template.vue
index 3dae05ccc69..0f68bf92048 100644
--- a/app/assets/javascripts/search/sidebar/components/filters_template.vue
+++ b/app/assets/javascripts/search/sidebar/components/filters_template.vue
@@ -21,9 +21,6 @@ export default {
computed: {
...mapState(['sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
- hrClasses() {
- return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
- },
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@@ -40,14 +37,15 @@ export default {
this.resetQuery();
},
},
+ HR_DEFAULT_CLASSES,
};
</script>
<template>
<gl-form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking">
- <hr v-if="!useSidebarNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<slot></slot>
- <hr v-if="!useSidebarNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<div class="gl-display-flex gl-align-items-center gl-mt-4">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 919bd2b2e49..dbd52978163 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -2,13 +2,15 @@
// 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/index';
+import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants';
import { confidentialFilterData } from './confidentiality_filter/data';
import { statusFilterData } from './status_filter/data';
import ConfidentialityFilter from './confidentiality_filter/index.vue';
import { labelFilterData } from './label_filter/data';
+import { archivedFilterData } from './archived_filter/data';
import LabelFilter from './label_filter/index.vue';
import StatusFilter from './status_filter/index.vue';
+import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@@ -19,11 +21,12 @@ export default {
ConfidentialityFilter,
LabelFilter,
FiltersTemplate,
+ ArchivedFilter,
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
- ...mapState(['useSidebarNavigation']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
showConfidentialityFilter() {
return Object.values(confidentialFilterData.scopes).includes(this.currentScope);
},
@@ -33,7 +36,15 @@ export default {
showLabelFilter() {
return (
Object.values(labelFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchIssueLabelAggregation
+ this.glFeatures.searchIssueLabelAggregation &&
+ this.searchType === SEARCH_TYPE_ADVANCED
+ );
+ },
+ showArchivedFilter() {
+ return (
+ Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
+ this.glFeatures.searchIssuesHideArchivedProjects &&
+ this.searchType === SEARCH_TYPE_ADVANCED
);
},
showDivider() {
@@ -52,6 +63,8 @@ export default {
<hr v-if="showConfidentialityFilter && showDivider" :class="hrClasses" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
<hr v-if="showLabelFilter && showDivider" :class="hrClasses" />
- <label-filter v-if="showLabelFilter" />
+ <label-filter v-if="showLabelFilter" class="gl-mb-5" />
+ <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
+ <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index ca1503d7c64..784207cc702 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -74,7 +74,7 @@ export default {
</script>
<template>
- <div v-if="hasBuckets" class="gl-my-0 language-filter-checkbox">
+ <div v-if="hasBuckets" class="language-filter-checkbox">
<h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ $options.languageFilterData.header }}
</h5>
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 bc5b797dd56..2845eb2049b 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -1,18 +1,49 @@
<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, SEARCH_TYPE_ADVANCED } from '../constants';
+import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
+import { archivedFilterData } from './archived_filter/data';
+import ArchivedFilter from './archived_filter/index.vue';
export default {
name: 'MergeRequestsFilters',
components: {
StatusFilter,
FiltersTemplate,
+ ArchivedFilter,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapGetters(['currentScope']),
+ ...mapState(['useSidebarNavigation', 'searchType']),
+ showArchivedFilter() {
+ return (
+ Object.values(archivedFilterData.scopes).includes(this.currentScope) &&
+ this.glFeatures.searchMergeRequestsHideArchivedProjects &&
+ this.searchType === SEARCH_TYPE_ADVANCED
+ );
+ },
+ showStatusFilter() {
+ return Object.values(statusFilterData.scopes).includes(this.currentScope);
+ },
+ showDivider() {
+ return !this.useSidebarNavigation;
+ },
+ hrClasses() {
+ return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
+ },
},
};
</script>
<template>
<filters-template>
- <status-filter class="gl-mb-5" />
+ <status-filter v-if="showStatusFilter" class="gl-mb-5" />
+ <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
+ <archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/notes_filters.vue b/app/assets/javascripts/search/sidebar/components/notes_filters.vue
new file mode 100644
index 00000000000..3a9f582d554
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/notes_filters.vue
@@ -0,0 +1,18 @@
+<script>
+import ArchivedFilter from './archived_filter/index.vue';
+import FiltersTemplate from './filters_template.vue';
+
+export default {
+ name: 'NotesFilters',
+ components: {
+ ArchivedFilter,
+ FiltersTemplate,
+ },
+};
+</script>
+
+<template>
+ <filters-template>
+ <archived-filter class="gl-mb-5" />
+ </filters-template>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
index e8d5de4d769..a4c1119736f 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
@@ -57,7 +57,7 @@ export default {
</script>
<template>
- <nav data-testid="search-filter">
+ <nav data-testid="search-filter" class="gl-border-none">
<gl-nav vertical pills>
<gl-nav-item
v-for="(item, scope) in navigation"
@@ -81,6 +81,5 @@ export default {
</span>
</gl-nav-item>
</gl-nav>
- <hr class="gl-mt-5 gl-mx-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" />
</nav>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue b/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue
new file mode 100644
index 00000000000..e966b8d877e
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlDrawer } from '@gitlab/ui';
+import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
+import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'SmallScreenDrawerNavigation',
+ components: {
+ GlDrawer,
+ DomElementListener,
+ },
+ i18n: {
+ smallScreenFiltersDrawerHeader: s__('GlobalSearch|Filters'),
+ },
+ data() {
+ return {
+ openSmallScreenFilters: false,
+ };
+ },
+ computed: {
+ getDrawerHeaderHeight() {
+ if (!this.openSmallScreenFilters) return '0';
+ return getContentWrapperHeight();
+ },
+ },
+ methods: {
+ closeSmallScreenFilters() {
+ this.openSmallScreenFilters = false;
+ },
+ toggleSmallScreenFilters() {
+ this.openSmallScreenFilters = !this.openSmallScreenFilters;
+ },
+ },
+ DRAWER_Z_INDEX,
+};
+</script>
+<template>
+ <dom-element-listener selector="#js-open-mobile-filters" @click="toggleSmallScreenFilters">
+ <gl-drawer
+ :header-height="getDrawerHeaderHeight"
+ :z-index="$options.DRAWER_Z_INDEX"
+ variant="sidebar"
+ class="small-screen-drawer-navigation"
+ :open="openSmallScreenFilters"
+ @close="closeSmallScreenFilters"
+ >
+ <template #title>
+ <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
+ {{ $options.i18n.smallScreenFiltersDrawerHeader }}
+ </h2>
+ </template>
+ <template #default>
+ <div>
+ <slot></slot>
+ </div>
+ </template>
+ </gl-drawer>
+ </dom-element-listener>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 01d0aad206c..19df875c292 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -2,6 +2,8 @@ export const SCOPE_ISSUES = 'issues';
export const SCOPE_MERGE_REQUESTS = 'merge_requests';
export const SCOPE_BLOB = 'blobs';
export const SCOPE_PROJECTS = 'projects';
+export const SCOPE_NOTES = 'notes';
+export const SCOPE_COMMITS = 'commits';
export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
@@ -19,3 +21,7 @@ export const ONLY_SHOW_MD = ['gl-display-none', 'gl-md-display-block'];
export const TRACKING_ACTION_CLICK = 'search:filters:click';
export const TRACKING_LABEL_APPLY = 'Apply Filters';
export const TRACKING_LABEL_RESET = 'Reset Filters';
+
+export const SEARCH_TYPE_BASIC = 'basic';
+export const SEARCH_TYPE_ADVANCED = 'advanced';
+export const SEARCH_TYPE_ZOEKT = 'zoekt';
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index 415f6f7454c..3a699355dc9 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -8,9 +8,11 @@ export const sidebarInitState = () => {
const el = document.getElementById('js-search-sidebar');
if (!el) return {};
- const { navigationJson } = el.dataset;
+ const { navigationJson, searchType } = el.dataset;
+
const navigationJsonParsed = JSON.parse(navigationJson);
- return { navigationJsonParsed };
+
+ return { navigationJsonParsed, searchType };
};
export const initSidebar = (store) => {
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index a68a0f75a2f..211bbaf82cd 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -138,7 +138,7 @@ export const setLabelFilterSearch = ({ commit }, { value }) => {
export const fetchSidebarCount = ({ commit, state }) => {
const promises = Object.values(state.navigation).map((navItem) => {
// active nav item has count already so we skip it
- if (!navItem.active) {
+ if (!navItem.active && navItem.count_link) {
return axios
.get(navItem.count_link)
.then(({ data: { count } }) => {
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index 65bb21f1b8a..b248681f053 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -33,7 +33,7 @@ export default {
state.frequentItems[key] = data;
},
[types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) {
- const item = { ...state.navigation[key], count };
+ const item = { ...state.navigation[key], count, count_link: null };
state.navigation = { ...state.navigation, [key]: item };
},
[types.REQUEST_AGGREGATIONS](state) {
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 5407b08fa83..b4cd2af65ba 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
-const createState = ({ query, navigation, useSidebarNavigation }) => ({
+const createState = ({ query, navigation, useSidebarNavigation, searchType }) => ({
urlQuery: cloneDeep(query),
query,
groups: [],
@@ -21,6 +21,7 @@ const createState = ({ query, navigation, useSidebarNavigation }) => ({
data: [],
},
searchLabelString: '',
+ searchType,
});
export default createState;
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index 2f02ef3475c..b15f89fc6a2 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -68,7 +68,8 @@ export const setFrequentItemToLS = (key, data, itemData) => {
frequentItems.sort((a, b) => {
if (a.frequency > b.frequency) {
return -1;
- } else if (a.frequency < b.frequency) {
+ }
+ if (a.frequency < b.frequency) {
return 1;
}
return b.lastUsed - a.lastUsed;
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index c7d89113895..32d46a0d4af 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,13 +1,18 @@
<script>
import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
+import Api from '~/api';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import { SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT } from '~/tracking/constants';
import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
-import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from './constants';
+import {
+ AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
+ TAB_VULNERABILITY_MANAGEMENT_INDEX,
+} from './constants';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
@@ -123,6 +128,11 @@ export default {
dismissAlert() {
this.errorMessage = '';
},
+ tabChange(value) {
+ if (value === TAB_VULNERABILITY_MANAGEMENT_INDEX) {
+ Api.trackRedisHllUserEvent(SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT);
+ }
+ },
},
autoDevopsEnabledAlertStorageKey: AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
};
@@ -167,6 +177,7 @@ export default {
data-testid="security-configuration-container"
sync-active-tab-with-query-params
lazy
+ @input="tabChange"
>
<gl-tab
data-testid="security-testing-tab"
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index b427820144d..da213b0ed43 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -1,5 +1,6 @@
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__ } from '~/locale';
+import ContinuousVulnerabilityScan from '~/security_configuration/components/continuous_vulnerability_scan.vue';
import {
REPORT_TYPE_SAST,
@@ -210,6 +211,7 @@ export const securityFeatures = [
configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
anchor: 'dependency-scanning',
+ slotComponent: ContinuousVulnerabilityScan,
},
{
name: CONTAINER_SCANNING_NAME,
@@ -326,3 +328,5 @@ export const TEMP_PROVIDER_URLS = {
[__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
SecureFlag: 'https://www.secureflag.com/',
};
+
+export const TAB_VULNERABILITY_MANAGEMENT_INDEX = 1;
diff --git a/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue
new file mode 100644
index 00000000000..61cbde2107c
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue
@@ -0,0 +1,127 @@
+<script>
+import { GlBadge, GlIcon, GlToggle, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
+import ProjectSetContinuousVulnerabilityScanning from '~/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ name: 'ContinuousVulnerabilityscan',
+ components: { GlBadge, GlIcon, GlToggle, GlLink, GlSprintf, GlAlert },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['continuousVulnerabilityScansEnabled', 'projectFullPath'],
+ i18n: {
+ badgeLabel: __('Experiment'),
+ title: s__('CVS|Continuous Vulnerability Scan'),
+ description: s__(
+ 'CVS|Detect vulnerabilities outside a pipeline as new data is added to the GitLab Advisory Database.',
+ ),
+ learnMore: __('Learn more'),
+ testingAgreementMessage: s__(
+ 'CVS|By enabling this feature, you accept the %{linkStart}Testing Terms of Use%{linkEnd}',
+ ),
+ },
+ props: {
+ feature: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ toggleValue: this.continuousVulnerabilityScansEnabled,
+ errorMessage: '',
+ isAlertDismissed: false,
+ };
+ },
+ computed: {
+ isFeatureConfigured() {
+ return this.feature.available && this.feature.configured;
+ },
+ shouldShowAlert() {
+ return this.errorMessage && !this.isAlertDismissed;
+ },
+ },
+ methods: {
+ reportError(error) {
+ this.errorMessage = error;
+ this.isAlertDismissed = false;
+ },
+ async toggleCVS(checked) {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: ProjectSetContinuousVulnerabilityScanning,
+ variables: {
+ input: {
+ projectPath: this.projectFullPath,
+ enable: checked,
+ },
+ },
+ });
+
+ const { errors } = data.projectSetContinuousVulnerabilityScanning;
+
+ if (errors.length > 0) {
+ this.reportError(errors[0].message);
+ }
+ if (data.projectSetContinuousVulnerabilityScanning !== null) {
+ this.toggleValue = checked;
+ }
+ } catch (error) {
+ this.reportError(error);
+ }
+ },
+ },
+ CVSHelpPagePath: helpPagePath(
+ 'user/application_security/continuous_vulnerability_scanning/index',
+ ),
+ experimentHelpPagePath: helpPagePath('policy/experiment-beta-support', { anchor: 'experiment' }),
+};
+</script>
+
+<template>
+ <div v-if="glFeatures.dependencyScanningOnAdvisoryIngestion">
+ <h4 class="gl-font-base gl-m-0 gl-mt-6">
+ {{ $options.i18n.title }}
+ <gl-badge
+ ref="badge"
+ :href="$options.experimentHelpPagePath"
+ target="_blank"
+ size="sm"
+ variant="neutral"
+ class="gl-cursor-pointer"
+ >{{ $options.i18n.badgeLabel }}</gl-badge
+ >
+ </h4>
+ <gl-alert
+ v-if="shouldShowAlert"
+ class="gl-mb-5 gl-mt-2"
+ variant="danger"
+ @dismiss="isAlertDismissed = true"
+ >{{ errorMessage }}</gl-alert
+ >
+ <gl-toggle
+ class="gl-mt-5"
+ :disabled="!isFeatureConfigured"
+ :value="toggleValue"
+ :label="s__('CVS|Toggle CVS')"
+ label-position="hidden"
+ @change="toggleCVS"
+ />
+
+ <p class="gl-mb-0 gl-mt-5">
+ {{ $options.i18n.description }}
+ <gl-link :href="$options.CVSHelpPagePath" target="_blank">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ <br />
+ <gl-sprintf :message="$options.i18n.testingAgreementMessage">
+ <template #link="{ content }">
+ <gl-link href="https://about.gitlab.com/handbook/legal/testing-agreement" target="_blank">
+ {{ content }} <gl-icon name="external-link" />
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index a757657339b..7f0a049a6ad 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -73,6 +73,9 @@ export default {
hasSecondary() {
return Boolean(this.feature.secondary);
},
+ hasSlotComponent() {
+ return Boolean(this.feature.slotComponent);
+ },
// This condition is a temporary hack to not display any wrong information
// until this BE Bug is fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/350307.
// More Information: https://gitlab.com/gitlab-org/gitlab/-/issues/350307#note_825447417
@@ -215,5 +218,9 @@ export default {
{{ $options.i18n.configurationGuide }}
</gl-button>
</div>
+
+ <div v-if="hasSlotComponent">
+ <component :is="feature.slotComponent" :feature="feature" />
+ </div>
</gl-card>
</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql b/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql
new file mode 100644
index 00000000000..79f4316d106
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql
@@ -0,0 +1,8 @@
+mutation ProjectSetContinuousVulnerabilityScanning(
+ $input: ProjectSetContinuousVulnerabilityScanningInput!
+) {
+ projectSetContinuousVulnerabilityScanning(input: $input) {
+ continuousVulnerabilityScanningEnabled
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index aa3c9c87622..4b498091134 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -26,6 +26,7 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
+ continuousVulnerabilityScansEnabled,
} = el.dataset;
const { augmentedSecurityFeatures } = augmentFeatures(
@@ -43,6 +44,7 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
+ continuousVulnerabilityScansEnabled,
},
render(createElement) {
return createElement(SecurityConfigurationApp, {
diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js
index 72e6d870e13..7f0caf1af46 100644
--- a/app/assets/javascripts/security_configuration/utils.js
+++ b/app/assets/javascripts/security_configuration/utils.js
@@ -1,5 +1,6 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
+import { REPORT_TYPE_DAST } from '~/vue_shared/security_reports/constants';
/**
* This function takes in 3 arrays of objects, securityFeatures and features.
@@ -29,6 +30,10 @@ export const augmentFeatures = (securityFeatures, features = []) => {
augmented.secondary = { ...augmented.secondary, ...featuresByType[feature.secondary.type] };
}
+ if (augmented.type === REPORT_TYPE_DAST && !augmented.onDemandAvailable) {
+ delete augmented.badge;
+ }
+
if (augmented.badge && augmented.metaInfoPath) {
augmented.badge.badgeHref = augmented.metaInfoPath;
}
diff --git a/app/assets/javascripts/sentry/index.js b/app/assets/javascripts/sentry/index.js
index cf6a79fe939..940caea3322 100644
--- a/app/assets/javascripts/sentry/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -1,35 +1,4 @@
import '../webpack';
+import { initSentry } from './init_sentry';
-import * as Sentry from 'sentrybrowser7';
-import SentryConfig from './sentry_config';
-
-const index = function index() {
- // Configuration for newer versions of Sentry SDK (v7)
- SentryConfig.init({
- dsn: gon.sentry_dsn,
- environment: gon.sentry_environment,
- currentUserId: gon.current_user_id,
- allowUrls:
- process.env.NODE_ENV === 'production'
- ? [gon.gitlab_url]
- : [gon.gitlab_url, 'webpack-internal://'],
- release: gon?.version,
- tags: {
- revision: gon?.revision,
- feature_category: gon?.feature_category,
- page: document?.body?.dataset?.page,
- },
- });
-};
-
-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`
-// in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
-
-// eslint-disable-next-line no-underscore-dangle
-window._Sentry = Sentry;
-
-export default index;
+initSentry();
diff --git a/app/assets/javascripts/sentry/init_sentry.js b/app/assets/javascripts/sentry/init_sentry.js
new file mode 100644
index 00000000000..dbd12dc36ce
--- /dev/null
+++ b/app/assets/javascripts/sentry/init_sentry.js
@@ -0,0 +1,77 @@
+import {
+ BrowserClient,
+ getCurrentHub,
+ defaultStackParser,
+ makeFetchTransport,
+ defaultIntegrations,
+
+ // exports
+ captureException,
+ captureMessage,
+ withScope,
+ SDK_VERSION,
+} from 'sentrybrowser';
+
+const initSentry = () => {
+ if (!gon?.sentry_dsn) {
+ return;
+ }
+
+ const hub = getCurrentHub();
+
+ const client = new BrowserClient({
+ // Sentry.init(...) options
+ dsn: gon.sentry_dsn,
+ release: gon.version,
+ allowUrls:
+ process.env.NODE_ENV === 'production'
+ ? [gon.gitlab_url]
+ : [gon.gitlab_url, 'webpack-internal://'],
+ environment: gon.sentry_environment,
+
+ // Browser tracing configuration
+ tracePropagationTargets: [/^\//], // only trace internal requests
+ tracesSampleRate: gon.sentry_clientside_traces_sample_rate || 0,
+
+ // This configuration imitates the Sentry.init() default configuration
+ // https://github.com/getsentry/sentry-javascript/blob/7.66.0/MIGRATION.md#explicit-client-options
+ transport: makeFetchTransport,
+ stackParser: defaultStackParser,
+ integrations: defaultIntegrations,
+ });
+
+ hub.bindClient(client);
+
+ hub.setTags({
+ revision: gon.revision,
+ feature_category: gon.feature_category,
+ page: document?.body?.dataset?.page,
+ });
+
+ if (gon.current_user_id) {
+ hub.setUser({
+ id: gon.current_user_id,
+ });
+ }
+
+ // The option `autoSessionTracking` is only avaialble on Sentry.init
+ // this manually starts a session in a similar way.
+ // See: https://github.com/getsentry/sentry-javascript/blob/7.66.0/packages/browser/src/sdk.ts#L204
+ hub.startSession({ ignoreDuration: true }); // `ignoreDuration` counts only the page view.
+ hub.captureSession();
+
+ // 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`
+ // in the browser, see app/views/layouts/_head.html.haml to find how it is imported.
+
+ // eslint-disable-next-line no-underscore-dangle
+ window._Sentry = {
+ captureException,
+ captureMessage,
+ withScope,
+ SDK_VERSION, // used to verify compatibility with the Sentry instance
+ };
+};
+
+export { initSentry };
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
index 0382827f82c..fbfd5d4f458 100644
--- a/app/assets/javascripts/sentry/sentry_browser_wrapper.js
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -5,6 +5,8 @@
// This module wraps methods used by our production code.
// Each export is names as we cannot export the entire namespace from *.
+
+/** @type {import('@sentry/core').captureException} */
export const captureException = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
@@ -12,6 +14,7 @@ export const captureException = (...args) => {
Sentry?.captureException(...args);
};
+/** @type {import('@sentry/core').captureMessage} */
export const captureMessage = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
@@ -19,6 +22,7 @@ export const captureMessage = (...args) => {
Sentry?.captureMessage(...args);
};
+/** @type {import('@sentry/core').withScope} */
export const withScope = (...args) => {
// eslint-disable-next-line no-underscore-dangle
const Sentry = window._Sentry;
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
deleted file mode 100644
index 80f087691f4..00000000000
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import * as Sentry from 'sentrybrowser7';
-
-const SentryConfig = {
- init(options = {}) {
- this.options = options;
-
- this.configure();
- if (this.options.currentUserId) this.setUser();
- },
-
- configure() {
- const { dsn, release, tags, allowUrls, environment } = this.options;
-
- Sentry.init({
- dsn,
- release,
- allowUrls,
- environment,
- });
-
- Sentry.setTags(tags);
- },
-
- setUser() {
- Sentry.setUser({
- id: this.options.currentUserId,
- });
- },
-};
-
-export default SentryConfig;
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index fe5b21713a2..da948cc85b6 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { InternalEvents } from '~/tracking';
import { __ } from './locale';
/**
@@ -47,6 +48,15 @@ export function toggleSection($section) {
}
}
+export function initTrackProductAnalyticsExpanded() {
+ const $analyticsSection = $('#js-product-analytics-settings');
+ $analyticsSection.on('click.toggleSection', '.js-settings-toggle', () => {
+ if (isExpanded($analyticsSection)) {
+ InternalEvents.track_event('user_viewed_cluster_configuration');
+ }
+ });
+}
+
export default function initSettingsPanels() {
$('.settings').each((i, elm) => {
const $section = $(elm);
@@ -64,4 +74,6 @@ export default function initSettingsPanels() {
}
}
});
+
+ initTrackProductAnalyticsExpanded();
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index 319699b88f3..cf77a5ca82c 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,6 +1,6 @@
<script>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { assigneesQueries } from '../../constants';
+import { assigneesQueries } from '../../queries/constants';
export default {
subscription: null,
diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
index 577c01c50ff..8a912b00df1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue
@@ -84,7 +84,8 @@ export default {
if (mergeLength === this.users.length) {
return '';
- } else if (mergeLength > 0) {
+ }
+ if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index ae81dcb95de..4ff12824008 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -6,7 +6,7 @@ import { TYPE_ALERT, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, n__ } from '~/locale';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { assigneesQueries } from '../../constants';
+import { assigneesQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarAssigneesRealtime from './assignees_realtime.vue';
import IssuableAssignees from './issuable_assignees.vue';
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
index b41d126be68..232cdcd2198 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_invite_members.vue
@@ -15,7 +15,7 @@ export default {
},
computed: {
triggerSource() {
- return `${this.issuableType}-assignee-dropdown`;
+ return `${this.issuableType}_assignee_dropdown`;
},
},
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 930e7ff12d9..ef7f12f273f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -1,4 +1,5 @@
<script>
+import { GlButton } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, sprintf } from '~/locale';
@@ -9,6 +10,7 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
+ GlButton,
AssigneeAvatarLink,
UserNameWithStatus,
},
@@ -97,10 +99,11 @@ export default {
</assignee-avatar-link>
</div>
</div>
- <div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">
- <button
- type="button"
- class="btn-link gl-button gl-reset-color!"
+ <div v-if="renderShowMoreSection" class="gl-hover-text-blue-800" data-testid="user-list-more">
+ <gl-button
+ category="tertiary"
+ size="small"
+ data-testid="user-list-more-button"
data-qa-selector="more_assignees_link"
@click="toggleShowLess"
>
@@ -108,7 +111,7 @@ export default {
{{ hiddenAssigneesLabel }}
</template>
<template v-else>{{ __('- show less') }}</template>
- </button>
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index 3038cec03eb..7a1853b1b46 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,9 +1,9 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { TYPE_ISSUE } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_TEST_CASE, IssuableTypeText } from '~/issues/constants';
import { __, sprintf } from '~/locale';
-import { confidentialityQueries } from '../../constants';
+import { confidentialityQueries } from '../../queries/constants';
export default {
i18n: {
@@ -11,7 +11,7 @@ export default {
'You are going to turn on confidentiality. Only %{context} members with %{strongStart}%{permissions}%{strongEnd} can view or be notified about this %{issuableType}.',
),
confidentialityOffWarning: __(
- 'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.',
+ 'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see%{commentText} this %{issuableType}.',
),
},
components: {
@@ -56,11 +56,17 @@ export default {
isIssue() {
return this.issuableType === TYPE_ISSUE;
},
+ isTestCase() {
+ return this.issuableType === TYPE_TEST_CASE;
+ },
+ isIssueOrTestCase() {
+ return this.isIssue || this.isTestCase;
+ },
context() {
- return this.isIssue ? __('project') : __('group');
+ return this.isIssueOrTestCase ? __('project') : __('group');
},
workspacePath() {
- return this.isIssue
+ return this.isIssueOrTestCase
? {
projectPath: this.fullPath,
}
@@ -73,6 +79,12 @@ export default {
? __('at least the Reporter role, the author, and assignees')
: __('at least the Reporter role');
},
+ issuableTypeText() {
+ return IssuableTypeText[this.issuableType];
+ },
+ commentText() {
+ return this.isTestCase ? '' : __(' and leave a comment on');
+ },
},
methods: {
submitForm() {
@@ -108,7 +120,7 @@ export default {
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
- issuableType: this.issuableType,
+ issuableType: this.issuableTypeText,
},
),
});
@@ -135,7 +147,8 @@ export default {
</strong>
</template>
<template #context>{{ context }}</template>
- <template #issuableType>{{ issuableType }}</template>
+ <template #commentText>{{ commentText }}</template>
+ <template #issuableType>{{ issuableTypeText }}</template>
</gl-sprintf>
</p>
<div class="sidebar-item-warning-message-actions">
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
index 9177baec246..295d37671cc 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -3,7 +3,8 @@ import produce from 'immer';
import Vue from 'vue';
import { createAlert } from '~/alert';
import { __, sprintf } from '~/locale';
-import { confidentialityQueries, Tracking } from '../../constants';
+import { Tracking } from '../../constants';
+import { confidentialityQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue';
diff --git a/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
index 3287539e502..7a488bb379f 100644
--- a/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
+++ b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
@@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
-import { referenceQueries } from '../../constants';
+import { referenceQueries } from '../../queries/constants';
import CopyableField from './copyable_field.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index 5a9545f3460..89bc4b126d6 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -4,7 +4,8 @@ import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
-import { dateFields, dateTypes, dueDateQueries, startDateQueries, Tracking } from '../../constants';
+import { dateFields, dateTypes, Tracking } from '../../constants';
+import { dueDateQueries, startDateQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
import SidebarInheritDate from './sidebar_inherit_date.vue';
diff --git a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
index 6db332a82da..576043963de 100644
--- a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
@@ -3,11 +3,8 @@ import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { logError } from '~/lib/logger';
import EscalationStatus from 'ee_else_ce/sidebar/components/incidents/escalation_status.vue';
-import {
- escalationStatusQuery,
- escalationStatusMutation,
- INCIDENTS_I18N as i18n,
-} from '../../constants';
+import { INCIDENTS_I18N as i18n } from '../../constants';
+import { escalationStatusQuery, escalationStatusMutation } from '../../queries/constants';
import { getStatusLabel } from '../../utils';
import SidebarEditableItem from '../sidebar_editable_item.vue';
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
index 03ace6286e0..3ab7757d34d 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
@@ -19,7 +19,8 @@ export const dropdownButtonText = (state, getters) => {
if (!selectedLabels.length) {
return state.dropdownButtonText || __('Label');
- } else if (selectedLabels.length > 1) {
+ }
+ if (selectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: selectedLabels[0].title,
remainingLabelCount: selectedLabels.length - 1,
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
index 53582aacabd..a513c247be7 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
@@ -101,7 +101,8 @@ export default {
buttonText() {
if (!this.localSelectedLabels.length) {
return this.dropdownButtonText || __('Label');
- } else if (this.localSelectedLabels.length > 1) {
+ }
+ if (this.localSelectedLabels.length > 1) {
return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
firstLabelName: this.localSelectedLabels[0].title,
remainingLabelCount: this.localSelectedLabels.length - 1,
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index 45778640957..93e3cfba309 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -1,4 +1,5 @@
<script>
+import { get } from 'lodash';
import {
GlAlert,
GlTooltipDirective,
@@ -11,8 +12,7 @@ import produce from 'immer';
import { createAlert } from '~/alert';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '../../../constants';
-import createLabelMutation from './graphql/create_label.mutation.graphql';
+import { workspaceLabelsQueries, workspaceCreateLabelMutation } from '../../../queries/constants';
import { DEFAULT_LABEL_COLOR } from './constants';
const errorMessage = __('Error creating label.');
@@ -68,13 +68,19 @@ export default {
return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
},
mutationVariables() {
- const attributePath = this.labelCreateType === WORKSPACE_GROUP ? 'groupPath' : 'projectPath';
-
- return {
+ const variables = {
title: this.labelTitle,
color: this.selectedColor,
- [attributePath]: this.attrWorkspacePath,
};
+
+ if (this.labelCreateType) {
+ const attributePath =
+ this.labelCreateType === WORKSPACE_GROUP ? 'groupPath' : 'projectPath';
+
+ return { ...variables, [attributePath]: this.attrWorkspacePath };
+ }
+
+ return variables;
},
},
methods: {
@@ -88,7 +94,7 @@ export default {
this.selectedColor = this.getColorCode(color);
},
updateLabelsInCache(store, label) {
- const { query } = workspaceLabelsQueries[this.workspaceType];
+ const { query, dataPath } = workspaceLabelsQueries[this.workspaceType];
const sourceData = store.readQuery({
query,
@@ -97,7 +103,7 @@ export default {
const collator = new Intl.Collator('en');
const data = produce(sourceData, (draftData) => {
- const { nodes } = draftData.workspace.labels;
+ const { nodes } = get(draftData, dataPath);
nodes.push(label);
nodes.sort((a, b) => collator.compare(a.title, b.title));
});
@@ -114,7 +120,7 @@ export default {
const {
data: { labelCreate },
} = await this.$apollo.mutate({
- mutation: createLabelMutation,
+ mutation: workspaceCreateLabelMutation[this.workspaceType],
variables: this.mutationVariables,
update: (
store,
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
index 19fe78aca87..fc8834a97d4 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
@@ -4,7 +4,7 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '../../../constants';
+import { workspaceLabelsQueries } from '../../../queries/constants';
import LabelItem from './label_item.vue';
export default {
@@ -135,6 +135,16 @@ export default {
this.handleLabelClick(this.visibleLabels[0]);
}
},
+ handleFocus(event, index) {
+ if (index === 0 && event.target.classList.contains('is-focused')) {
+ event.target.classList.remove('is-focused');
+
+ // Focus next element (if available) as the first item was already focused.
+ if (event.target.parentNode?.nextElementSibling?.querySelector('button')) {
+ event.target.parentNode.nextElementSibling.querySelector('button').focus();
+ }
+ }
+ },
},
};
</script>
@@ -157,6 +167,7 @@ export default {
:active="shouldHighlightFirstItem && index === 0"
active-class="is-focused"
data-testid="labels-list"
+ @focus.native.capture="handleFocus($event, index)"
@click.native.capture.stop="handleLabelClick(label)"
>
<label-item :label="label" />
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
index e67e704ffb8..d6b43698766 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
@@ -13,7 +13,13 @@ export default {
},
footerManageLabelTitle: {
type: String,
- required: true,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ showManageLabelsItem() {
+ return this.footerManageLabelTitle && this.labelsManagePath;
},
},
};
@@ -28,7 +34,12 @@ export default {
>
{{ footerCreateLabelTitle }}
</gl-dropdown-item>
- <gl-dropdown-item :href="labelsManagePath" @click.capture.native.stop>
+ <gl-dropdown-item
+ v-if="showManageLabelsItem"
+ data-testid="manage-labels-button"
+ :href="labelsManagePath"
+ @click.capture.native.stop
+ >
{{ footerManageLabelTitle }}
</gl-dropdown-item>
</div>
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index 74c3f08a47b..f9a9cc316c1 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -7,7 +7,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';
import { __ } from '~/locale';
-import { issuableLabelsQueries } from '../../../constants';
+import { issuableLabelsQueries } from '../../../queries/constants';
import SidebarEditableItem from '../../sidebar_editable_item.vue';
import { DEBOUNCE_DROPDOWN_DELAY, VARIANT_SIDEBAR } from './constants';
import DropdownContents from './dropdown_contents.vue';
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 7b288e15a3e..99d36a61632 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -138,13 +138,10 @@ export default {
</a>
</div>
</div>
- <div v-if="hasMoreParticipants" class="participants-more hide-collapsed">
- <gl-button
- variant="link"
- button-text-classes="gl-text-secondary"
- @click="toggleMoreParticipants"
- >{{ toggleLabel }}</gl-button
- >
+ <div v-if="hasMoreParticipants" class="hide-collapsed">
+ <gl-button category="tertiary" size="small" @click="toggleMoreParticipants">{{
+ toggleLabel
+ }}</gl-button>
</div>
</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 b0556e22a8d..b764d660d63 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -1,6 +1,6 @@
<script>
import { __ } from '~/locale';
-import { participantsQueries } from '../../constants';
+import { participantsQueries } from '../../queries/constants';
import Participants from './participants.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
index 88a74784dd2..415c40b4779 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue
@@ -52,7 +52,8 @@ export default {
if (mergeLength === this.users.length) {
return '';
- } else if (mergeLength > 0) {
+ }
+ if (mergeLength > 0) {
return sprintf(__('%{mergeLength}/%{usersLength} can merge'), {
mergeLength,
usersLength: this.users.length,
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
index 50b4284cde0..c9450244b40 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
@@ -20,13 +20,13 @@ import {
defaultEpicSort,
dropdowni18nText,
epicIidPattern,
- issuableAttributesQueries,
IssuableAttributeState,
IssuableAttributeType,
IssuableAttributeTypeKeyMap,
LocalizedIssuableAttributeType,
noAttributeId,
} from 'ee_else_ce/sidebar/constants';
+import { issuableAttributesQueries } from 'ee_else_ce/sidebar/queries/constants';
import { createAlert } from '~/alert';
import { PathIdSeparator } from '~/related_issues/constants';
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 4721c6fee61..7fde43a360d 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -11,10 +11,10 @@ import {
dropdowni18nText,
LocalizedIssuableAttributeType,
IssuableAttributeTypeKeyMap,
- issuableAttributesQueries,
IssuableAttributeType,
Tracking,
} from 'ee_else_ce/sidebar/constants';
+import { issuableAttributesQueries } from 'ee_else_ce/sidebar/queries/constants';
import SidebarDropdown from './sidebar_dropdown.vue';
import SidebarEditableItem from './sidebar_editable_item.vue';
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index d6e1847aecb..568962cddc7 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -13,7 +13,8 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
-import { subscribedQueries, Tracking } from '../../constants';
+import { Tracking } from '../../constants';
+import { subscribedQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
const ICON_ON = 'notifications';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 465f971717f..ac05ae3896b 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -42,11 +42,14 @@ export default {
divClass() {
if (this.showComparisonState) {
return 'compare';
- } else if (this.showEstimateOnlyState) {
+ }
+ if (this.showEstimateOnlyState) {
return 'estimate-only';
- } else if (this.showSpentOnlyState) {
+ }
+ if (this.showSpentOnlyState) {
return 'spend-only';
- } else if (this.showNoTimeTrackingState) {
+ }
+ if (this.showNoTimeTrackingState) {
return 'no-tracking';
}
@@ -55,9 +58,11 @@ export default {
spanClass() {
if (this.showComparisonState) {
return '';
- } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
+ }
+ if (this.showEstimateOnlyState || this.showSpentOnlyState) {
return 'bold';
- } else if (this.showNoTimeTrackingState) {
+ }
+ if (this.showNoTimeTrackingState) {
return 'no-value collapse-truncated-title gl-pt-2 gl-px-3 gl-font-sm';
}
@@ -66,11 +71,14 @@ export default {
text() {
if (this.showComparisonState) {
return `${this.timeSpentHumanReadable} / ${this.timeEstimateHumanReadable}`;
- } else if (this.showEstimateOnlyState) {
+ }
+ if (this.showEstimateOnlyState) {
return `-- / ${this.timeEstimateHumanReadable}`;
- } else if (this.showSpentOnlyState) {
+ }
+ if (this.showSpentOnlyState) {
return `${this.timeSpentHumanReadable} / --`;
- } else if (this.showNoTimeTrackingState) {
+ }
+ if (this.showNoTimeTrackingState) {
return __('None');
}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 70d8024f46a..9bd4c7f5c68 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -7,7 +7,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/issues/constants';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
-import { timelogQueries } from '../../constants';
+import { timelogQueries } from '../../queries/constants';
import deleteTimelogMutation from '../../queries/delete_timelog.mutation.graphql';
const TIME_DATE_FORMAT = 'mmmm d, yyyy, HH:MM ("UTC:" o)';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
index 1d427a871e1..aff592d48e0 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -12,7 +12,8 @@ import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
-import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '../../constants';
+import { HOW_TO_TRACK_TIME } from '../../constants';
+import { timeTrackingQueries } from '../../queries/constants';
import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
@@ -122,9 +123,11 @@ export default {
// 3. issuableIid and fullPath are not provided
if (!this.issuableType || !timeTrackingQueries[this.issuableType]) {
return true;
- } else if (this.initialTimeTracking) {
+ }
+ if (this.initialTimeTracking) {
return true;
- } else if (!this.issuableIid || !this.fullPath) {
+ }
+ if (!this.issuableIid || !this.fullPath) {
return true;
}
return false;
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index 551d306a9c4..1099dcb832f 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -6,7 +6,8 @@ import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
-import { todoQueries, TodoMutationTypes, todoMutations } from '../../constants';
+import { TodoMutationTypes } from '../../constants';
+import { todoQueries, todoMutations } from '../../queries/constants';
import { todoLabel } from '../../utils';
import TodoButton from './todo_button.vue';
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 0f82182c6e2..f13f613733b 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,173 +1,10 @@
import { invert } from 'lodash';
import { s__, __, sprintf } from '~/locale';
-import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
-import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
-import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
-import {
- TYPE_ALERT,
- TYPE_EPIC,
- TYPE_ISSUE,
- TYPE_MERGE_REQUEST,
- TYPE_TEST_CASE,
- WORKSPACE_GROUP,
- WORKSPACE_PROJECT,
-} from '~/issues/constants';
-import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import issuableDatesUpdatedSubscription from '../graphql_shared/subscriptions/work_item_dates.subscription.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';
-import updateEpicLabelsMutation from './components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import groupLabelsQuery from './components/labels/labels_select_widget/graphql/group_labels.query.graphql';
-import issueLabelsQuery from './components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
-import mergeRequestLabelsQuery from './components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql';
-import projectLabelsQuery from './components/labels/labels_select_widget/graphql/project_labels.query.graphql';
-import epicConfidentialQuery from './queries/epic_confidential.query.graphql';
-import epicDueDateQuery from './queries/epic_due_date.query.graphql';
-import epicParticipantsQuery from './queries/epic_participants.query.graphql';
-import epicReferenceQuery from './queries/epic_reference.query.graphql';
-import epicStartDateQuery from './queries/epic_start_date.query.graphql';
-import epicSubscribedQuery from './queries/epic_subscribed.query.graphql';
-import epicTodoQuery from './queries/epic_todo.query.graphql';
-import issuableAssigneesSubscription from './queries/issuable_assignees.subscription.graphql';
-import issueConfidentialQuery from './queries/issue_confidential.query.graphql';
-import issueDueDateQuery from './queries/issue_due_date.query.graphql';
-import issueReferenceQuery from './queries/issue_reference.query.graphql';
-import issueSubscribedQuery from './queries/issue_subscribed.query.graphql';
-import issueTimeTrackingQuery from './queries/issue_time_tracking.query.graphql';
-import issueTodoQuery from './queries/issue_todo.query.graphql';
-import mergeRequestMilestone from './queries/merge_request_milestone.query.graphql';
-import mergeRequestReferenceQuery from './queries/merge_request_reference.query.graphql';
-import mergeRequestSubscribed from './queries/merge_request_subscribed.query.graphql';
-import mergeRequestTimeTrackingQuery from './queries/merge_request_time_tracking.query.graphql';
-import mergeRequestTodoQuery from './queries/merge_request_todo.query.graphql';
-import todoCreateMutation from './queries/todo_create.mutation.graphql';
-import todoMarkDoneMutation from './queries/todo_mark_done.mutation.graphql';
-import updateEpicConfidentialMutation from './queries/update_epic_confidential.mutation.graphql';
-import updateEpicDueDateMutation from './queries/update_epic_due_date.mutation.graphql';
-import updateEpicStartDateMutation from './queries/update_epic_start_date.mutation.graphql';
-import updateEpicSubscriptionMutation from './queries/update_epic_subscription.mutation.graphql';
-import updateIssueConfidentialMutation from './queries/update_issue_confidential.mutation.graphql';
-import updateIssueDueDateMutation from './queries/update_issue_due_date.mutation.graphql';
-import updateIssueSubscriptionMutation from './queries/update_issue_subscription.mutation.graphql';
-import mergeRequestMilestoneMutation from './queries/update_merge_request_milestone.mutation.graphql';
-import updateMergeRequestLabelsMutation from './queries/update_merge_request_labels.mutation.graphql';
-import updateMergeRequestSubscriptionMutation from './queries/update_merge_request_subscription.mutation.graphql';
-import getAlertAssignees from './queries/get_alert_assignees.query.graphql';
-import getIssueAssignees from './queries/get_issue_assignees.query.graphql';
-import issueParticipantsQuery from './queries/get_issue_participants.query.graphql';
-import getIssueTimelogsQuery from './queries/get_issue_timelogs.query.graphql';
-import getMergeRequestAssignees from './queries/get_mr_assignees.query.graphql';
-import getMergeRequestParticipants from './queries/get_mr_participants.query.graphql';
-import getMrTimelogsQuery from './queries/get_mr_timelogs.query.graphql';
-import updateIssueAssigneesMutation from './queries/update_issue_assignees.mutation.graphql';
-import updateMergeRequestAssigneesMutation from './queries/update_mr_assignees.mutation.graphql';
-import getEscalationStatusQuery from './queries/escalation_status.query.graphql';
-import updateEscalationStatusMutation from './queries/update_escalation_status.mutation.graphql';
-import groupMilestonesQuery from './queries/group_milestones.query.graphql';
-import projectIssueMilestoneMutation from './queries/project_issue_milestone.mutation.graphql';
-import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
-import projectMilestonesQuery from './queries/project_milestones.query.graphql';
export const defaultEpicSort = 'TITLE_ASC';
export const epicIidPattern = /^&(?<iid>\d+)$/;
-export const assigneesQueries = {
- [TYPE_ISSUE]: {
- query: getIssueAssignees,
- subscription: issuableAssigneesSubscription,
- mutation: updateIssueAssigneesMutation,
- },
- [TYPE_MERGE_REQUEST]: {
- query: getMergeRequestAssignees,
- mutation: updateMergeRequestAssigneesMutation,
- },
- [TYPE_ALERT]: {
- query: getAlertAssignees,
- mutation: updateAlertAssigneesMutation,
- },
-};
-
-export const participantsQueries = {
- [TYPE_ISSUE]: {
- query: issueParticipantsQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: getMergeRequestParticipants,
- },
- [TYPE_EPIC]: {
- query: epicParticipantsQuery,
- },
- [TYPE_ALERT]: {
- query: '',
- skipQuery: true,
- },
-};
-
-export const userSearchQueries = {
- [TYPE_ISSUE]: {
- query: userSearchQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: userSearchWithMRPermissionsQuery,
- },
-};
-
-export const confidentialityQueries = {
- [TYPE_ISSUE]: {
- query: issueConfidentialQuery,
- mutation: updateIssueConfidentialMutation,
- },
- [TYPE_EPIC]: {
- query: epicConfidentialQuery,
- mutation: updateEpicConfidentialMutation,
- },
-};
-
-export const referenceQueries = {
- [TYPE_ISSUE]: {
- query: issueReferenceQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestReferenceQuery,
- },
- [TYPE_EPIC]: {
- query: epicReferenceQuery,
- },
-};
-
-export const workspaceLabelsQueries = {
- [WORKSPACE_PROJECT]: {
- query: projectLabelsQuery,
- },
- [WORKSPACE_GROUP]: {
- query: groupLabelsQuery,
- },
-};
-
-export const issuableLabelsQueries = {
- [TYPE_ISSUE]: {
- issuableQuery: issueLabelsQuery,
- mutation: updateIssueLabelsMutation,
- mutationName: 'updateIssue',
- },
- [TYPE_MERGE_REQUEST]: {
- issuableQuery: mergeRequestLabelsQuery,
- mutation: updateMergeRequestLabelsMutation,
- mutationName: 'mergeRequestSetLabels',
- },
- [TYPE_EPIC]: {
- issuableQuery: epicLabelsQuery,
- mutation: updateEpicLabelsMutation,
- mutationName: 'updateEpic',
- },
- [TYPE_TEST_CASE]: {
- issuableQuery: issueLabelsQuery,
- mutation: updateTestCaseLabelsMutation,
- mutationName: 'updateTestCaseLabels',
- },
-};
-
export const dateTypes = {
start: 'startDate',
due: 'dueDate',
@@ -186,91 +23,13 @@ export const dateFields = {
},
};
-export const subscribedQueries = {
- [TYPE_ISSUE]: {
- query: issueSubscribedQuery,
- mutation: updateIssueSubscriptionMutation,
- },
- [TYPE_EPIC]: {
- query: epicSubscribedQuery,
- mutation: updateEpicSubscriptionMutation,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestSubscribed,
- mutation: updateMergeRequestSubscriptionMutation,
- },
-};
-
export const Tracking = {
editEvent: 'click_edit_button',
rightSidebarLabel: 'right_sidebar',
};
-export const timeTrackingQueries = {
- [TYPE_ISSUE]: {
- query: issueTimeTrackingQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestTimeTrackingQuery,
- },
-};
-
-export const dueDateQueries = {
- [TYPE_ISSUE]: {
- query: issueDueDateQuery,
- mutation: updateIssueDueDateMutation,
- subscription: issuableDatesUpdatedSubscription,
- },
- [TYPE_EPIC]: {
- query: epicDueDateQuery,
- mutation: updateEpicDueDateMutation,
- },
-};
-
-export const startDateQueries = {
- [TYPE_EPIC]: {
- query: epicStartDateQuery,
- mutation: updateEpicStartDateMutation,
- },
-};
-
-export const timelogQueries = {
- [TYPE_ISSUE]: {
- query: getIssueTimelogsQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: getMrTimelogsQuery,
- },
-};
-
export const noAttributeId = null;
-export const issuableMilestoneQueries = {
- [TYPE_ISSUE]: {
- query: projectIssueMilestoneQuery,
- mutation: projectIssueMilestoneMutation,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestMilestone,
- mutation: mergeRequestMilestoneMutation,
- },
-};
-
-export const milestonesQueries = {
- [TYPE_ISSUE]: {
- query: {
- [WORKSPACE_GROUP]: groupMilestonesQuery,
- [WORKSPACE_PROJECT]: projectMilestonesQuery,
- },
- },
- [TYPE_MERGE_REQUEST]: {
- query: {
- [WORKSPACE_GROUP]: groupMilestonesQuery,
- [WORKSPACE_PROJECT]: projectMilestonesQuery,
- },
- },
-};
-
export const IssuableAttributeType = {
Milestone: 'milestone',
};
@@ -285,35 +44,11 @@ export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
-export const issuableAttributesQueries = {
- [IssuableAttributeType.Milestone]: {
- current: issuableMilestoneQueries,
- list: milestonesQueries,
- },
-};
-
-export const todoQueries = {
- [TYPE_EPIC]: {
- query: epicTodoQuery,
- },
- [TYPE_ISSUE]: {
- query: issueTodoQuery,
- },
- [TYPE_MERGE_REQUEST]: {
- query: mergeRequestTodoQuery,
- },
-};
-
export const TodoMutationTypes = {
Create: 'create',
MarkDone: 'mark-done',
};
-export const todoMutations = {
- [TodoMutationTypes.Create]: todoCreateMutation,
- [TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
-};
-
export function dropdowni18nText(issuableAttribute, issuableType) {
return {
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
@@ -362,9 +97,6 @@ export function dropdowni18nText(issuableAttribute, issuableType) {
};
}
-export const escalationStatusQuery = getEscalationStatusQuery;
-export const escalationStatusMutation = updateEscalationStatusMutation;
-
export const HOW_TO_TRACK_TIME = __('How to track time');
export const statusDropdownOptions = [
diff --git a/app/assets/javascripts/sidebar/queries/constants.js b/app/assets/javascripts/sidebar/queries/constants.js
new file mode 100644
index 00000000000..0844abc4599
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/constants.js
@@ -0,0 +1,291 @@
+import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
+import userAutocompleteQuery from '~/graphql_shared/queries/project_autocomplete_users.query.graphql';
+import userAutocompleteWithMRPermissionsQuery from '~/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql';
+import issuableDatesUpdatedSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
+import {
+ TYPE_ALERT,
+ TYPE_EPIC,
+ TYPE_ISSUE,
+ TYPE_MERGE_REQUEST,
+ TYPE_TEST_CASE,
+ WORKSPACE_GROUP,
+ 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 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';
+import updateEpicLabelsMutation from '../components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import groupLabelsQuery from '../components/labels/labels_select_widget/graphql/group_labels.query.graphql';
+import issueLabelsQuery from '../components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
+import mergeRequestLabelsQuery from '../components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql';
+import projectLabelsQuery from '../components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import { IssuableAttributeType, TodoMutationTypes } from '../constants';
+import epicConfidentialQuery from './epic_confidential.query.graphql';
+import epicDueDateQuery from './epic_due_date.query.graphql';
+import epicParticipantsQuery from './epic_participants.query.graphql';
+import epicReferenceQuery from './epic_reference.query.graphql';
+import epicStartDateQuery from './epic_start_date.query.graphql';
+import epicSubscribedQuery from './epic_subscribed.query.graphql';
+import epicTodoQuery from './epic_todo.query.graphql';
+import issuableAssigneesSubscription from './issuable_assignees.subscription.graphql';
+import issueConfidentialQuery from './issue_confidential.query.graphql';
+import issueDueDateQuery from './issue_due_date.query.graphql';
+import issueReferenceQuery from './issue_reference.query.graphql';
+import issueSubscribedQuery from './issue_subscribed.query.graphql';
+import issueTimeTrackingQuery from './issue_time_tracking.query.graphql';
+import issueTodoQuery from './issue_todo.query.graphql';
+import mergeRequestMilestone from './merge_request_milestone.query.graphql';
+import mergeRequestReferenceQuery from './merge_request_reference.query.graphql';
+import mergeRequestSubscribed from './merge_request_subscribed.query.graphql';
+import mergeRequestTimeTrackingQuery from './merge_request_time_tracking.query.graphql';
+import mergeRequestTodoQuery from './merge_request_todo.query.graphql';
+import todoCreateMutation from './todo_create.mutation.graphql';
+import todoMarkDoneMutation from './todo_mark_done.mutation.graphql';
+import updateEpicConfidentialMutation from './update_epic_confidential.mutation.graphql';
+import updateEpicDueDateMutation from './update_epic_due_date.mutation.graphql';
+import updateEpicStartDateMutation from './update_epic_start_date.mutation.graphql';
+import updateEpicSubscriptionMutation from './update_epic_subscription.mutation.graphql';
+import updateIssueConfidentialMutation from './update_issue_confidential.mutation.graphql';
+import updateIssueDueDateMutation from './update_issue_due_date.mutation.graphql';
+import updateIssueSubscriptionMutation from './update_issue_subscription.mutation.graphql';
+import mergeRequestMilestoneMutation from './update_merge_request_milestone.mutation.graphql';
+import updateMergeRequestLabelsMutation from './update_merge_request_labels.mutation.graphql';
+import updateMergeRequestSubscriptionMutation from './update_merge_request_subscription.mutation.graphql';
+import getAlertAssignees from './get_alert_assignees.query.graphql';
+import getIssueAssignees from './get_issue_assignees.query.graphql';
+import issueParticipantsQuery from './get_issue_participants.query.graphql';
+import getIssueTimelogsQuery from './get_issue_timelogs.query.graphql';
+import getMergeRequestAssignees from './get_mr_assignees.query.graphql';
+import getMergeRequestParticipants from './get_mr_participants.query.graphql';
+import getMrTimelogsQuery from './get_mr_timelogs.query.graphql';
+import updateIssueAssigneesMutation from './update_issue_assignees.mutation.graphql';
+import updateMergeRequestAssigneesMutation from './update_mr_assignees.mutation.graphql';
+import getEscalationStatusQuery from './escalation_status.query.graphql';
+import updateEscalationStatusMutation from './update_escalation_status.mutation.graphql';
+import groupMilestonesQuery from './group_milestones.query.graphql';
+import projectIssueMilestoneMutation from './project_issue_milestone.mutation.graphql';
+import projectIssueMilestoneQuery from './project_issue_milestone.query.graphql';
+import projectMilestonesQuery from './project_milestones.query.graphql';
+import testCaseConfidentialQuery from './test_case_confidential.query.graphql';
+import updateTestCaseConfidentialMutation from './update_test_case_confidential.mutation.graphql';
+
+export const assigneesQueries = {
+ [TYPE_ISSUE]: {
+ query: getIssueAssignees,
+ subscription: issuableAssigneesSubscription,
+ mutation: updateIssueAssigneesMutation,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: getMergeRequestAssignees,
+ mutation: updateMergeRequestAssigneesMutation,
+ },
+ [TYPE_ALERT]: {
+ query: getAlertAssignees,
+ mutation: updateAlertAssigneesMutation,
+ },
+};
+
+export const participantsQueries = {
+ [TYPE_ISSUE]: {
+ query: issueParticipantsQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: getMergeRequestParticipants,
+ },
+ [TYPE_EPIC]: {
+ query: epicParticipantsQuery,
+ },
+ [TYPE_ALERT]: {
+ query: '',
+ skipQuery: true,
+ },
+};
+
+export const userSearchQueries = {
+ [TYPE_ISSUE]: {
+ query: userAutocompleteQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: userAutocompleteWithMRPermissionsQuery,
+ },
+};
+
+export const confidentialityQueries = {
+ [TYPE_ISSUE]: {
+ query: issueConfidentialQuery,
+ mutation: updateIssueConfidentialMutation,
+ },
+ [TYPE_EPIC]: {
+ query: epicConfidentialQuery,
+ mutation: updateEpicConfidentialMutation,
+ },
+ [TYPE_TEST_CASE]: {
+ query: testCaseConfidentialQuery,
+ mutation: updateTestCaseConfidentialMutation,
+ },
+};
+
+export const referenceQueries = {
+ [TYPE_ISSUE]: {
+ query: issueReferenceQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestReferenceQuery,
+ },
+ [TYPE_EPIC]: {
+ query: epicReferenceQuery,
+ },
+};
+
+export const workspaceLabelsQueries = {
+ [WORKSPACE_PROJECT]: {
+ query: projectLabelsQuery,
+ dataPath: 'workspace.labels',
+ },
+ [WORKSPACE_GROUP]: {
+ query: groupLabelsQuery,
+ dataPath: 'workspace.labels',
+ },
+ abuseReport: {
+ query: abuseReportLabelsQuery,
+ dataPath: 'labels',
+ },
+};
+
+export const workspaceCreateLabelMutation = {
+ [WORKSPACE_PROJECT]: createGroupOrProjectLabelMutation,
+ [WORKSPACE_GROUP]: createGroupOrProjectLabelMutation,
+ abuseReport: createAbuseReportLabelMutation,
+};
+
+export const issuableLabelsQueries = {
+ [TYPE_ISSUE]: {
+ issuableQuery: issueLabelsQuery,
+ mutation: updateIssueLabelsMutation,
+ mutationName: 'updateIssue',
+ },
+ [TYPE_MERGE_REQUEST]: {
+ issuableQuery: mergeRequestLabelsQuery,
+ mutation: updateMergeRequestLabelsMutation,
+ mutationName: 'mergeRequestSetLabels',
+ },
+ [TYPE_EPIC]: {
+ issuableQuery: epicLabelsQuery,
+ mutation: updateEpicLabelsMutation,
+ mutationName: 'updateEpic',
+ },
+ [TYPE_TEST_CASE]: {
+ issuableQuery: issueLabelsQuery,
+ mutation: updateTestCaseLabelsMutation,
+ mutationName: 'updateTestCaseLabels',
+ },
+};
+
+export const subscribedQueries = {
+ [TYPE_ISSUE]: {
+ query: issueSubscribedQuery,
+ mutation: updateIssueSubscriptionMutation,
+ },
+ [TYPE_EPIC]: {
+ query: epicSubscribedQuery,
+ mutation: updateEpicSubscriptionMutation,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestSubscribed,
+ mutation: updateMergeRequestSubscriptionMutation,
+ },
+};
+
+export const timeTrackingQueries = {
+ [TYPE_ISSUE]: {
+ query: issueTimeTrackingQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestTimeTrackingQuery,
+ },
+};
+
+export const dueDateQueries = {
+ [TYPE_ISSUE]: {
+ query: issueDueDateQuery,
+ mutation: updateIssueDueDateMutation,
+ subscription: issuableDatesUpdatedSubscription,
+ },
+ [TYPE_EPIC]: {
+ query: epicDueDateQuery,
+ mutation: updateEpicDueDateMutation,
+ },
+};
+
+export const startDateQueries = {
+ [TYPE_EPIC]: {
+ query: epicStartDateQuery,
+ mutation: updateEpicStartDateMutation,
+ },
+};
+
+export const timelogQueries = {
+ [TYPE_ISSUE]: {
+ query: getIssueTimelogsQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: getMrTimelogsQuery,
+ },
+};
+
+export const issuableMilestoneQueries = {
+ [TYPE_ISSUE]: {
+ query: projectIssueMilestoneQuery,
+ mutation: projectIssueMilestoneMutation,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestMilestone,
+ mutation: mergeRequestMilestoneMutation,
+ },
+};
+
+export const milestonesQueries = {
+ [TYPE_ISSUE]: {
+ query: {
+ [WORKSPACE_GROUP]: groupMilestonesQuery,
+ [WORKSPACE_PROJECT]: projectMilestonesQuery,
+ },
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: {
+ [WORKSPACE_GROUP]: groupMilestonesQuery,
+ [WORKSPACE_PROJECT]: projectMilestonesQuery,
+ },
+ },
+};
+
+export const issuableAttributesQueries = {
+ [IssuableAttributeType.Milestone]: {
+ current: issuableMilestoneQueries,
+ list: milestonesQueries,
+ },
+};
+
+export const todoQueries = {
+ [TYPE_EPIC]: {
+ query: epicTodoQuery,
+ },
+ [TYPE_ISSUE]: {
+ query: issueTodoQuery,
+ },
+ [TYPE_MERGE_REQUEST]: {
+ query: mergeRequestTodoQuery,
+ },
+};
+
+export const todoMutations = {
+ [TodoMutationTypes.Create]: todoCreateMutation,
+ [TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
+};
+
+export const escalationStatusQuery = getEscalationStatusQuery;
+
+export const escalationStatusMutation = updateEscalationStatusMutation;
diff --git a/app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql b/app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql
new file mode 100644
index 00000000000..d8959b5ce3f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/test_case_confidential.query.graphql
@@ -0,0 +1,9 @@
+query testCaseConfidential($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql
new file mode 100644
index 00000000000..4094907cb95
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_test_case_confidential.mutation.graphql
@@ -0,0 +1,9 @@
+mutation updateTestCaseConfidential($input: IssueSetConfidentialInput!) {
+ issuableSetConfidential: issueSetConfidential(input: $input) {
+ issuable: issue {
+ id
+ confidential
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/silent_mode_settings/components/app.vue b/app/assets/javascripts/silent_mode_settings/components/app.vue
new file mode 100644
index 00000000000..2dd0449448c
--- /dev/null
+++ b/app/assets/javascripts/silent_mode_settings/components/app.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlToggle, GlBadge } from '@gitlab/ui';
+import { updateApplicationSettings } from '~/rest_api';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import { sprintf, __, s__ } from '~/locale';
+
+export default {
+ name: 'SilentModeSettingsApp',
+ i18n: {
+ toggleLabel: s__('SilentMode|Enable silent mode'),
+ saveSuccess: s__('SilentMode|Silent mode %{status}'),
+ saveError: s__('SilentMode|There was an error updating the Silent Mode Settings.'),
+ enabled: __('enabled'),
+ disabled: __('disabled'),
+ experiment: __('Experiment'),
+ },
+ components: {
+ GlToggle,
+ GlBadge,
+ },
+ props: {
+ isSilentModeEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ silentModeEnabled: this.isSilentModeEnabled,
+ };
+ },
+ methods: {
+ updateSilentModeSettings() {
+ this.isLoading = true;
+
+ updateApplicationSettings({
+ silent_mode_enabled: this.silentModeEnabled,
+ })
+ .then(() => {
+ const status = this.silentModeEnabled
+ ? this.$options.i18n.enabled
+ : this.$options.i18n.disabled;
+ toast(sprintf(this.$options.i18n.saveSuccess, { status }));
+ })
+ .catch(() => {
+ createAlert({ message: this.$options.i18n.saveError });
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+<template>
+ <gl-toggle
+ v-model="silentModeEnabled"
+ label-id="silent-mode-toggle"
+ :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/silent_mode_settings/index.js b/app/assets/javascripts/silent_mode_settings/index.js
new file mode 100644
index 00000000000..b18f9c02964
--- /dev/null
+++ b/app/assets/javascripts/silent_mode_settings/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import SilentModeSettingsApp from './components/app.vue';
+
+Vue.use(Translate);
+
+export const initSilentModeSettings = () => {
+ const el = document.getElementById('js-silent-mode-settings');
+
+ if (!el) {
+ return false;
+ }
+
+ const { silentModeEnabled } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SilentModeSettingsApp, {
+ props: {
+ isSilentModeEnabled: parseBoolean(silentModeEnabled),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
index 0fdbc89a038..17312c2373b 100644
--- a/app/assets/javascripts/snippets/components/embed_dropdown.vue
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -1,12 +1,5 @@
<script>
-import {
- GlButton,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownText,
- GlFormInputGroup,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown, GlFormInputGroup, GlTooltipDirective } from '@gitlab/ui';
import { escape as esc } from 'lodash';
import { __ } from '~/locale';
@@ -17,9 +10,7 @@ const MSG_COPY = __('Copy');
export default {
components: {
GlButton,
- GlDropdown,
- GlDropdownSectionHeader,
- GlDropdownText,
+ GlDisclosureDropdown,
GlFormInputGroup,
},
directives: {
@@ -45,22 +36,16 @@ export default {
};
</script>
<template>
- <gl-dropdown
- right
- :text="$options.MSG_EMBED"
- menu-class="gl-px-1! gl-pb-5! gl-dropdown-menu-wide"
+ <gl-disclosure-dropdown
+ :auto-close="false"
+ fluid-width
+ placement="right"
+ :toggle-text="$options.MSG_EMBED"
>
<template v-for="{ name, value } in sections">
- <gl-dropdown-section-header :key="`header_${name}`" data-testid="header">{{
- name
- }}</gl-dropdown-section-header>
- <gl-dropdown-text
- :key="`input_${name}`"
- tag="div"
- class="gl-dropdown-text-py-0 gl-dropdown-text-block"
- data-testid="input"
- >
- <gl-form-input-group :value="value" readonly select-on-click :label="name">
+ <div :key="name" :data-testid="`section-${name}`" class="gl-px-4 gl-py-2">
+ <h5 class="gl-font-sm gl-mt-1 gl-mb-2" data-testid="header">{{ name }}</h5>
+ <gl-form-input-group class="gl-w-31" :value="value" readonly select-on-click :label="name">
<template #append>
<gl-button
v-gl-tooltip.hover
@@ -73,7 +58,7 @@ export default {
/>
</template>
</gl-form-input-group>
- </gl-dropdown-text>
+ </div>
</template>
- </gl-dropdown>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/snippets/utils/blob.js b/app/assets/javascripts/snippets/utils/blob.js
index a228d6111ce..97f21654aae 100644
--- a/app/assets/javascripts/snippets/utils/blob.js
+++ b/app/assets/javascripts/snippets/utils/blob.js
@@ -34,7 +34,8 @@ const diff = ({ content, path }, origBlob) => {
content,
filePath: path,
};
- } else if (origBlob.path !== path || origBlob.content !== content) {
+ }
+ if (origBlob.path !== path || origBlob.content !== content) {
return {
action: origBlob.path === path ? SNIPPET_BLOB_ACTION_UPDATE : SNIPPET_BLOB_ACTION_MOVE,
previousPath: origBlob.path,
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
index 1589f4978e1..02cf36fb053 100644
--- a/app/assets/javascripts/super_sidebar/components/brand_logo.vue
+++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
@@ -26,20 +26,28 @@ export default {
<template>
<a
- v-gl-tooltip:super-sidebar.hover.noninteractive.bottom.ds500="$options.i18n.homepage"
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
class="brand-logo"
:href="rootPath"
- :title="$options.i18n.homepage"
data-track-action="click_link"
data-track-label="gitlab_logo_link"
data-track-property="nav_core_menu"
>
+ <span class="gl-sr-only">{{ $options.i18n.homepage }}</span>
+ <!-- eslint-disable @gitlab/vue-require-i18n-attribute-strings -->
<img
v-if="logoUrl"
+ alt=""
data-testid="brand-header-custom-logo"
:src="logoUrl"
class="gl-h-6 gl-max-w-full"
/>
- <span v-else v-safe-html="$options.logo" data-testid="brand-header-default-logo"></span>
+ <!-- eslint-enable @gitlab/vue-require-i18n-attribute-strings -->
+ <span
+ v-else
+ v-safe-html="$options.logo"
+ aria-hidden
+ data-testid="brand-header-default-logo"
+ ></span>
</a>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_header.vue b/app/assets/javascripts/super_sidebar/components/context_header.vue
deleted file mode 100644
index 11b9840a409..00000000000
--- a/app/assets/javascripts/super_sidebar/components/context_header.vue
+++ /dev/null
@@ -1,56 +0,0 @@
-<script>
-import { GlTruncate, GlAvatar, GlIcon } from '@gitlab/ui';
-
-export default {
- components: {
- GlTruncate,
- GlAvatar,
- GlIcon,
- },
- props: {
- /*
- * Contains metadata about the current view, e.g. `id`, `title` and `avatar`
- */
- context: {
- type: Object,
- required: true,
- },
- tag: {
- type: String,
- required: false,
- default: 'div',
- },
- },
- computed: {
- avatarShape() {
- return this.context.avatar_shape || 'rect';
- },
- },
-};
-</script>
-
-<template>
- <component
- :is="tag"
- class="border-top border-bottom gl-border-gray-a-08! gl-display-flex gl-align-items-center gl-gap-3 gl-font-weight-bold gl-w-full gl-h-8 gl-px-4 gl-flex-shrink-0"
- >
- <span
- v-if="context.icon"
- class="gl-avatar avatar-container gl-bg-t-gray-a-08 icon-avatar rect-avatar s24"
- >
- <gl-icon class="gl-text-gray-700" :name="context.icon" :size="16" />
- </span>
- <gl-avatar
- v-else
- :size="24"
- :shape="avatarShape"
- :entity-name="context.title"
- :entity-id="context.id"
- :src="context.avatar"
- />
- <div class="gl-flex-grow-1 gl-overflow-auto gl-text-gray-900">
- <gl-truncate :text="context.title" />
- </div>
- <slot name="end"></slot>
- </component>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
deleted file mode 100644
index d4aa11b6e04..00000000000
--- a/app/assets/javascripts/super_sidebar/components/context_switcher.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<script>
-import * as Sentry from '@sentry/browser';
-import { GlDisclosureDropdown, GlSearchBoxByType, GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import searchUserProjectsAndGroups from '../graphql/queries/search_user_groups_and_projects.query.graphql';
-import { trackContextAccess, formatContextSwitcherItems } from '../utils';
-import NavItem from './nav_item.vue';
-import ProjectsList from './projects_list.vue';
-import GroupsList from './groups_list.vue';
-import ContextSwitcherToggle from './context_switcher_toggle.vue';
-
-export default {
- i18n: {
- contextNavigation: s__('Navigation|Context navigation'),
- switchTo: s__('Navigation|Switch context'),
- searchPlaceholder: s__('Navigation|Search your projects or groups'),
- searchingLabel: s__('Navigation|Retrieving search results'),
- searchError: s__('Navigation|There was an error fetching search results.'),
- },
- apollo: {
- groupsAndProjects: {
- query: searchUserProjectsAndGroups,
- manual: true,
- variables() {
- return {
- username: this.username,
- search: this.searchString,
- };
- },
- result(response) {
- this.hasError = false;
- try {
- const {
- data: {
- projects: { nodes: projects },
- user: {
- groups: { nodes: groups },
- },
- },
- } = response;
-
- this.projects = formatContextSwitcherItems(projects);
- this.groups = formatContextSwitcherItems(groups);
- } catch (e) {
- this.handleError(e);
- }
- },
- error(e) {
- this.handleError(e);
- },
- skip() {
- return !this.searchString;
- },
- },
- },
- components: {
- GlDisclosureDropdown,
- ContextSwitcherToggle,
- GlSearchBoxByType,
- GlLoadingIcon,
- GlAlert,
- NavItem,
- ProjectsList,
- GroupsList,
- },
- inject: ['contextSwitcherLinks'],
- props: {
- username: {
- type: String,
- required: true,
- },
- projectsPath: {
- type: String,
- required: true,
- },
- groupsPath: {
- type: String,
- required: true,
- },
- currentContext: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- contextHeader: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- searchString: '',
- projects: [],
- groups: [],
- hasError: false,
- isOpen: false,
- };
- },
- computed: {
- isSearch() {
- return Boolean(this.searchString);
- },
- isSearching() {
- return this.$apollo.queries.groupsAndProjects.loading;
- },
- },
- watch: {
- isOpen(isOpen) {
- this.$emit('toggle', isOpen);
-
- if (isOpen) {
- this.focusInput();
- }
- },
- },
- created() {
- if (this.currentContext.namespace) {
- trackContextAccess(this.username, this.currentContext);
- }
- },
- methods: {
- close() {
- this.$refs['disclosure-dropdown'].close();
- },
- focusInput() {
- this.$refs['search-box'].focusInput();
- },
- handleError(e) {
- Sentry.captureException(e);
- this.hasError = true;
- },
- onDisclosureDropdownShown() {
- this.isOpen = true;
- },
- onDisclosureDropdownHidden() {
- this.isOpen = false;
- },
- },
- DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
-};
-</script>
-
-<template>
- <gl-disclosure-dropdown
- ref="disclosure-dropdown"
- class="context-switcher gl-w-full"
- placement="center"
- @shown="onDisclosureDropdownShown"
- @hidden="onDisclosureDropdownHidden"
- >
- <template #toggle>
- <context-switcher-toggle :context="contextHeader" :expanded="isOpen" />
- </template>
- <div aria-hidden="true" class="gl-font-sm gl-font-weight-bold gl-px-4 gl-pt-3 gl-pb-4">
- {{ $options.i18n.switchTo }}
- </div>
- <div class="gl-p-1 gl-border-t gl-border-b gl-border-gray-50 gl-bg-white">
- <gl-search-box-by-type
- ref="search-box"
- v-model="searchString"
- class="context-switcher-search-box"
- :placeholder="$options.i18n.searchPlaceholder"
- :debounce="$options.DEFAULT_DEBOUNCE_AND_THROTTLE_MS"
- borderless
- />
- </div>
- <gl-loading-icon
- v-if="isSearching"
- class="gl-mt-5"
- size="md"
- :label="$options.i18n.searchingLabel"
- />
- <gl-alert v-else-if="hasError" variant="danger" :dismissible="false" class="gl-m-2">
- {{ $options.i18n.searchError }}
- </gl-alert>
- <nav v-else :aria-label="$options.i18n.contextNavigation" data-testid="context-navigation">
- <ul class="gl-p-0 gl-m-0 gl-list-style-none">
- <li v-if="!isSearch">
- <ul
- :aria-label="$options.i18n.switchTo"
- class="gl-border-b gl-border-gray-50 gl-px-0 gl-py-2"
- >
- <nav-item
- v-for="item in contextSwitcherLinks"
- :key="item.link"
- :item="item"
- :link-classes="{ [item.link_classes]: item.link_classes }"
- is-subitem
- />
- </ul>
- </li>
- <projects-list
- :username="username"
- :view-all-link="projectsPath"
- :is-search="isSearch"
- :search-results="projects"
- />
- <groups-list
- class="gl-border-t gl-border-gray-50"
- :username="username"
- :view-all-link="groupsPath"
- :is-search="isSearch"
- :search-results="groups"
- />
- </ul>
- </nav>
- </gl-disclosure-dropdown>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
deleted file mode 100644
index faa7eba6470..00000000000
--- a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
+++ /dev/null
@@ -1,43 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import ContextHeader from './context_header.vue';
-
-export default {
- components: {
- GlIcon,
- ContextHeader,
- },
- props: {
- /*
- * Contains metadata about the current view, e.g. `id`, `title` and `avatar`
- */
- context: {
- type: Object,
- required: true,
- },
- expanded: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- collapseIcon() {
- return this.expanded ? 'chevron-up' : 'chevron-down';
- },
- },
-};
-</script>
-
-<template>
- <context-header
- :context="context"
- tag="button"
- type="button"
- class="context-switcher-toggle gl-p-0 gl-bg-transparent gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-border-0 gl-box-shadow-none gl-text-left"
- data-testid="context-switcher"
- >
- <template #end>
- <gl-icon class="gl-text-gray-400" :name="collapseIcon" />
- </template>
- </context-header>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index 3645606515f..d1e96479631 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -1,7 +1,7 @@
<script>
import {
GlDisclosureDropdown,
- GlTooltip,
+ GlTooltipDirective,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
} from '@gitlab/ui';
@@ -14,7 +14,7 @@ import {
import { DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
// Left offset required for the dropdown to be aligned with the super sidebar
-const DROPDOWN_X_OFFSET_BASE = -147;
+const DROPDOWN_X_OFFSET_BASE = -179;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
@@ -22,9 +22,11 @@ export default {
GlDisclosureDropdown,
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
- GlTooltip,
InviteMembersTrigger,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
i18n: {
createNew: __('Create new...'),
},
@@ -59,45 +61,35 @@ export default {
</script>
<template>
- <div>
- <gl-disclosure-dropdown
- category="tertiary"
- icon="plus"
- no-caret
- text-sr-only
- :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"
- >
- <gl-disclosure-dropdown-group
- v-for="(group, index) in groups"
- :key="group.name"
- :bordered="index !== 0"
- :group="group"
- >
- <template v-for="groupItem in group.items">
- <invite-members-trigger
- v-if="isInvitedMembers(groupItem)"
- :key="`${groupItem.text}-trigger`"
- trigger-source="top-nav"
- :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN"
- />
- <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" />
- </template>
- </gl-disclosure-dropdown-group>
- </gl-disclosure-dropdown>
- <gl-tooltip
- v-if="!dropdownOpen"
- :target="`#${$options.toggleId}`"
- placement="bottom"
- container="#super-sidebar"
- noninteractive
+ <gl-disclosure-dropdown
+ v-gl-tooltip:super-sidebar.hover.bottom="dropdownOpen ? '' : $options.i18n.createNew"
+ category="tertiary"
+ icon="plus"
+ no-caret
+ text-sr-only
+ :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"
+ >
+ <gl-disclosure-dropdown-group
+ v-for="(group, index) in groups"
+ :key="group.name"
+ :bordered="index !== 0"
+ :group="group"
>
- {{ $options.i18n.createNew }}
- </gl-tooltip>
- </div>
+ <template v-for="groupItem in group.items">
+ <invite-members-trigger
+ v-if="isInvitedMembers(groupItem)"
+ :key="`${groupItem.text}-trigger`"
+ trigger-source="top_nav"
+ :trigger-element="$options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN"
+ />
+ <gl-disclosure-dropdown-item v-else :key="groupItem.text" :item="groupItem" />
+ </template>
+ </gl-disclosure-dropdown-group>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
index fa7960da2f4..e73b9b275ee 100644
--- a/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/flyout_menu.vue
@@ -2,6 +2,23 @@
import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
import NavItem from './nav_item.vue';
+// Flyout menus are shown when the MenuSection's title is hovered with the mouse.
+// Their position is dynamically calculated with floating-ui.
+//
+// Since flyout menus show all NavItems of a section, they can be very long and
+// a user might want to move their mouse diagonally from the section title down
+// to last nav item in the flyout. But this mouse movement over other sections
+// would loose hover and close the flyout, opening another section's flyout.
+// To avoid this annoyance, our flyouts come with a "diagonal tolerance". This
+// is an area between the current mouse position and the top- and bottom-left
+// corner of the flyout itself. While the mouse stays within this area and
+// reaches the flyout before a timer expires, the native browser hover stays
+// within the component.
+// This is done with an transparent SVG positioned left of the flyout menu,
+// overlapping the sidebar. The SVG itself ignores pointer events but its two
+// triangles, one above the section title, one below, do listen to events,
+// keeping hover.
+
export default {
name: 'FlyoutMenu',
components: { NavItem },
@@ -15,13 +32,45 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ currentMouseX: 0,
+ flyoutX: 0,
+ flyoutY: 0,
+ flyoutHeight: 0,
+ hoverTimeoutId: null,
+ showSVG: true,
+ targetRect: null,
+ };
+ },
cleanupFunction: undefined,
+ computed: {
+ topSVGPoints() {
+ const x = (this.currentMouseX / this.targetRect.width) * 100;
+ let y = ((this.targetRect.top - this.flyoutY) / this.flyoutHeight) * 100;
+ y += 1; // overlap title to not loose hover
+
+ return `${x}, ${y} 100, 0 100, ${y}`;
+ },
+ bottomSVGPoints() {
+ const x = (this.currentMouseX / this.targetRect.width) * 100;
+ let y = ((this.targetRect.bottom - this.flyoutY) / this.flyoutHeight) * 100;
+ y -= 1; // overlap title to not loose hover
+
+ return `${x}, ${y} 100, ${y} 100, 100`;
+ },
+ },
+ created() {
+ const target = document.querySelector(`#${this.targetId}`);
+ target.addEventListener('mousemove', this.onMouseMove);
+ },
mounted() {
const target = document.querySelector(`#${this.targetId}`);
const flyout = document.querySelector(`#${this.targetId}-flyout`);
+ const sidebar = document.querySelector('#super-sidebar');
- function updatePosition() {
- return computePosition(target, flyout, {
+ const updatePosition = () =>
+ computePosition(target, flyout, {
middleware: [offset({ alignmentAxis: -12 }), flip(), shift()],
placement: 'right-start',
strategy: 'fixed',
@@ -30,13 +79,46 @@ export default {
left: `${x}px`,
top: `${y}px`,
});
+ this.flyoutX = x;
+ this.flyoutY = y;
+ this.flyoutHeight = flyout.clientHeight;
+
+ // Flyout coordinates are relative to the sidebar which can be
+ // shifted down by the performance-bar etc.
+ // Adjust viewport coordinates from getBoundingClientRect:
+ const targetRect = target.getBoundingClientRect();
+ const sidebarRect = sidebar.getBoundingClientRect();
+ this.targetRect = {
+ top: targetRect.top - sidebarRect.top,
+ bottom: targetRect.bottom - sidebarRect.top,
+ width: targetRect.width,
+ };
});
- }
this.$options.cleanupFunction = autoUpdate(target, flyout, updatePosition);
},
beforeUnmount() {
this.$options.cleanupFunction();
+ clearTimeout(this.hoverTimeoutId);
+ },
+ beforeDestroy() {
+ const target = document.querySelector(`#${this.targetId}`);
+ target.removeEventListener('mousemove', this.onMouseMove);
+ },
+ methods: {
+ startHoverTimeout() {
+ this.hoverTimeoutId = setTimeout(() => {
+ this.showSVG = false;
+ this.$emit('mouseleave');
+ }, 1000);
+ },
+ stopHoverTimeout() {
+ clearTimeout(this.hoverTimeoutId);
+ },
+ onMouseMove(e) {
+ // add some wiggle room to the left of mouse cursor
+ this.currentMouseX = Math.max(0, e.clientX - 5);
+ },
},
};
</script>
@@ -49,8 +131,8 @@ export default {
@mouseleave="$emit('mouseleave')"
>
<ul
- v-if="items.length > 0"
class="gl-min-w-20 gl-max-w-34 gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-100 gl-shadow-md gl-bg-white gl-p-2 gl-pb-1 gl-list-style-none"
+ @mouseenter="showSVG = false"
>
<nav-item
v-for="item of items"
@@ -61,5 +143,44 @@ export default {
@pin-remove="(itemId) => $emit('pin-remove', itemId)"
/>
</ul>
+ <svg
+ v-if="targetRect && showSVG"
+ :width="flyoutX"
+ :height="flyoutHeight"
+ viewBox="0 0 100 100"
+ preserveAspectRatio="none"
+ :style="{
+ top: flyoutY + 'px',
+ }"
+ >
+ <polygon
+ ref="topSVG"
+ :points="topSVGPoints"
+ fill="transparent"
+ @mouseenter="startHoverTimeout"
+ @mouseleave="stopHoverTimeout"
+ />
+ <polygon
+ ref="bottomSVG"
+ :points="bottomSVGPoints"
+ fill="transparent"
+ @mouseenter="startHoverTimeout"
+ @mouseleave="stopHoverTimeout"
+ />
+ </svg>
</div>
</template>
+
+<style scoped>
+svg {
+ pointer-events: none;
+
+ position: fixed;
+ right: 0;
+}
+
+svg polygon,
+svg rect {
+ pointer-events: auto;
+}
+</style>
diff --git a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue b/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue
deleted file mode 100644
index fe1a907bd91..00000000000
--- a/app/assets/javascripts/super_sidebar/components/frequent_items_list.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
-import {
- getItemsFromLocalStorage,
- removeItemFromLocalStorage,
- formatContextSwitcherItems,
-} from '../utils';
-import ItemsList from './items_list.vue';
-
-export default {
- components: {
- GlButton,
- ItemsList,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- pristineText: {
- type: String,
- required: true,
- },
- storageKey: {
- type: String,
- required: true,
- },
- maxItems: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- cachedFrequentItems: [],
- };
- },
- computed: {
- isEmpty() {
- return !this.cachedFrequentItems.length;
- },
- },
- created() {
- this.cachedFrequentItems = formatContextSwitcherItems(
- getItemsFromLocalStorage({
- storageKey: this.storageKey,
- maxItems: this.maxItems,
- }),
- );
- },
- methods: {
- handleItemRemove(item) {
- removeItemFromLocalStorage({
- storageKey: this.storageKey,
- item,
- });
-
- this.cachedFrequentItems = this.cachedFrequentItems.filter((i) => i.id !== item.id);
- },
- },
- i18n: {
- removeItem: __('Remove'),
- },
-};
-</script>
-
-<template>
- <li class="gl-py-3">
- <div
- data-testid="list-title"
- aria-hidden="true"
- class="gl-display-flex gl-align-items-center gl-text-transform-uppercase gl-text-secondary gl-font-weight-semibold gl-font-xs gl-line-height-12 gl-letter-spacing-06em gl-my-3"
- >
- <span class="gl-flex-grow-1 gl-px-3">{{ title }}</span>
- </div>
- <div
- v-if="isEmpty"
- data-testid="empty-text"
- class="gl-text-gray-500 gl-font-sm gl-my-3 gl-mx-3"
- >
- {{ pristineText }}
- </div>
- <items-list :aria-label="title" :items="cachedFrequentItems">
- <template #actions="{ item }">
- <gl-button
- v-gl-tooltip.right.viewport
- size="small"
- category="tertiary"
- icon="dash"
- class="show-on-focus-or-hover--target"
- :aria-label="$options.i18n.removeItem"
- :title="$options.i18n.removeItem"
- data-testid="item-remove"
- @click.stop.prevent="handleItemRemove(item)"
- />
- </template>
- <template #view-all-items>
- <slot name="view-all-items"></slot>
- </template>
- </items-list>
- </li>
-</template>
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 bd79962f1a1..b85b163cea9 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
@@ -5,6 +5,7 @@ import { GlDisclosureDropdownGroup, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import Tracking from '~/tracking';
import { getFormattedItem } from '../utils';
import {
@@ -18,6 +19,8 @@ import {
PATH_GROUP_TITLE,
GROUP_TITLES,
MAX_ROWS,
+ TRACKING_ACTIVATE_COMMAND_PALETTE,
+ TRACKING_HANDLE_LABEL_MAP,
} from './constants';
import SearchItem from './search_item.vue';
import { commandMapper, linksReducer, autocompleteQuery, fileMapper } from './utils';
@@ -29,6 +32,7 @@ export default {
GlLoadingIcon,
SearchItem,
},
+ mixins: [Tracking.mixin()],
inject: [
'commandPaletteCommands',
'commandPaletteLinks',
@@ -134,10 +138,15 @@ export default {
immediate: true,
},
handle: {
- handler() {
- this.debouncedSearch();
+ handler(value, oldValue) {
+ // Do not run search immediately on component creation
+ if (oldValue !== undefined) this.debouncedSearch();
+
+ // Track immediately on component creation
+ const label = TRACKING_HANDLE_LABEL_MAP[value] ?? 'unknown';
+ this.track(TRACKING_ACTIVATE_COMMAND_PALETTE, { label });
},
- immediate: false,
+ immediate: true,
},
},
updated() {
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
index a43e621da44..f6f4e36e43a 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/constants.js
@@ -6,6 +6,16 @@ export const PROJECT_HANDLE = ':';
export const ISSUE_HANDLE = '#';
export const PATH_HANDLE = '/';
+export const TRACKING_ACTIVATE_COMMAND_PALETTE = 'activate_command_palette';
+export const TRACKING_CLICK_COMMAND_PALETTE_ITEM = 'click_command_palette_item';
+export const TRACKING_HANDLE_LABEL_MAP = {
+ [COMMAND_HANDLE]: 'command',
+ [USER_HANDLE]: 'user',
+ [PROJECT_HANDLE]: 'project',
+ [PATH_HANDLE]: 'path',
+ // No ISSUE_HANDLE. See https://gitlab.com/gitlab-org/gitlab/-/issues/417434.
+};
+
export const COMMON_HANDLES = [COMMAND_HANDLE, USER_HANDLE, PROJECT_HANDLE];
export const SEARCH_OR_COMMAND_MODE_PLACEHOLDER = sprintf(
s__(
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
index 347a8ffb0b4..32abbbfd3c2 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/command_palette/utils.js
@@ -1,6 +1,11 @@
import { isNil, omitBy } from 'lodash';
import { objectToQuery, joinPaths } from '~/lib/utils/url_utility';
-import { SEARCH_SCOPE, GLOBAL_COMMANDS_GROUP_TITLE } from './constants';
+import { TRACKING_UNKNOWN_ID } from '~/super_sidebar/constants';
+import {
+ SEARCH_SCOPE,
+ GLOBAL_COMMANDS_GROUP_TITLE,
+ TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+} from './constants';
export const commandMapper = ({ name, items }) => {
// TODO: we filter out invite_members for now, because it is complicated to add the invite members modal here
@@ -12,18 +17,34 @@ export const commandMapper = ({ name, items }) => {
};
export const linksReducer = (acc, menuItem) => {
+ const trackingAttrs = ({ id, title }) => {
+ return {
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': id || TRACKING_UNKNOWN_ID,
+ ...(id
+ ? {}
+ : {
+ 'data-track-extra': JSON.stringify({ title }),
+ }),
+ },
+ };
+ };
+
acc.push({
text: menuItem.title,
keywords: menuItem.title,
icon: menuItem.icon,
href: menuItem.link,
+ ...trackingAttrs(menuItem),
});
if (menuItem.items?.length) {
- const items = menuItem.items.map(({ title, link }) => ({
- keywords: title,
- text: [menuItem.title, title].join(' > '),
- href: link,
+ const items = menuItem.items.map((item) => ({
+ keywords: item.title,
+ text: [menuItem.title, item.title].join(' > '),
+ href: item.link,
icon: menuItem.icon,
+ ...trackingAttrs(item),
}));
/* eslint-disable-next-line no-param-reassign */
@@ -37,6 +58,10 @@ export const fileMapper = (projectBlobPath, file) => {
icon: 'doc-code',
text: file,
href: joinPaths(projectBlobPath, file),
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'file',
+ },
};
};
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
index 382d844ceee..ddadd6856ca 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
@@ -2,6 +2,8 @@
import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils';
+import { TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import FrequentItem from './frequent_item.vue';
export default {
@@ -65,6 +67,12 @@ export default {
// validator, and the href field ensures it renders a link.
text: item.name,
href: item.webUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': item.id,
+ 'data-track-property': TRACKING_UNKNOWN_PANEL,
+ 'data-track-extra': JSON.stringify({ title: item.name }),
+ },
},
forRenderer: {
id: item.id,
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 b64f3ac52b2..4cfc329f8b8 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
@@ -18,14 +18,12 @@ import { sprintf } from '~/locale';
import { ARROW_DOWN_KEY, ARROW_UP_KEY, END_KEY, HOME_KEY, ESC_KEY } from '~/lib/utils/keys';
import {
MIN_SEARCH_TERM,
- SEARCH_GITLAB,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
} from '~/vue_shared/global_search/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import {
SEARCH_INPUT_DESCRIPTION,
@@ -52,10 +50,10 @@ export default {
name: 'GlobalSearchModal',
SEARCH_MODAL_ID,
i18n: {
- SEARCH_GITLAB,
SEARCH_DESCRIBED_BY_WITH_RESULTS,
SEARCH_DESCRIBED_BY_DEFAULT,
SEARCH_DESCRIBED_BY_UPDATED,
+ SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
SEARCH_RESULTS_LOADING,
SEARCH_RESULTS_SCOPE,
MIN_SEARCH_TERM,
@@ -72,7 +70,6 @@ export default {
CommandPaletteItems,
FakeSearchInput,
},
- mixins: [glFeatureFlagMixin()],
data() {
return {
nextFocusedItemIndex: null,
@@ -89,9 +86,6 @@ export default {
this.setSearch(value);
},
},
- searchPlaceholder() {
- return this.glFeatures?.commandPalette ? SEARCH_OR_COMMAND_MODE_PLACEHOLDER : SEARCH_GITLAB;
- },
showDefaultItems() {
return !this.searchText;
},
@@ -146,9 +140,8 @@ export default {
},
isCommandMode() {
return (
- this.glFeatures?.commandPalette &&
- (COMMON_HANDLES.includes(this.searchTextFirstChar) ||
- (this.searchContext.project && this.searchTextFirstChar === PATH_HANDLE))
+ COMMON_HANDLES.includes(this.searchTextFirstChar) ||
+ (this.searchContext?.project && this.searchTextFirstChar === PATH_HANDLE)
);
},
commandPaletteQuery() {
@@ -294,7 +287,7 @@ export default {
>
<form
role="search"
- :aria-label="searchPlaceholder"
+ :aria-label="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER"
class="gl-relative gl-rounded-base gl-w-full gl-pb-0"
>
<div class="gl-relative gl-bg-white gl-border-b gl-mb-n1 gl-p-3">
@@ -305,7 +298,7 @@ export default {
role="searchbox"
data-testid="global-search-input"
autocomplete="off"
- :placeholder="searchPlaceholder"
+ :placeholder="$options.i18n.SEARCH_OR_COMMAND_MODE_PLACEHOLDER"
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
borderless
@input="getAutocompleteOptions"
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 9a375837102..9167be5c1cc 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,6 +1,8 @@
<script>
import { GlDisclosureDropdownGroup } from '@gitlab/ui';
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';
export default {
name: 'DefaultPlaces',
@@ -18,7 +20,23 @@ export default {
group() {
return {
name: this.$options.i18n.PLACES,
- items: this.contextSwitcherLinks.map(({ title, link }) => ({ text: title, href: link })),
+ items: this.contextSwitcherLinks.map(({ title, link }) => ({
+ text: title,
+ href: link,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ // The label and property are hard-coded as unknown for now for
+ // parity with the existing corresponding context switcher items.
+ // Once the context switcher is removed, these can be changed.
+ 'data-track-label': TRACKING_UNKNOWN_ID,
+ 'data-track-property': TRACKING_UNKNOWN_PANEL,
+ 'data-track-extra': JSON.stringify({ title }),
+
+ // QA attributes
+ 'data-testid': 'places-item-link',
+ 'data-qa-places-item': title,
+ },
+ })),
};
},
},
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
index 6871dabc9a1..79be56f1427 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/store/getters.js
@@ -14,6 +14,7 @@ import {
SEARCH_RESULTS_ORDER,
} from '~/vue_shared/global_search/constants';
import { getFormattedItem } from '../utils';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import {
ICON_GROUP,
@@ -172,6 +173,10 @@ export const scopedSearchOptions = (state, getters) => {
scopeCategory: PROJECTS_CATEGORY,
icon: ICON_PROJECT,
href: getters.projectUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_project',
+ },
});
}
@@ -182,6 +187,10 @@ export const scopedSearchOptions = (state, getters) => {
scopeCategory: GROUPS_CATEGORY,
icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP,
href: getters.groupUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_group',
+ },
});
}
@@ -189,6 +198,10 @@ export const scopedSearchOptions = (state, getters) => {
text: 'scoped-in-all',
description: MSG_IN_ALL_GITLAB,
href: getters.allUrl,
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': 'scoped_in_all',
+ },
});
return items;
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/utils.js b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
index 11d1fa1ab95..2c369cbdf5f 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/utils.js
+++ b/app/assets/javascripts/super_sidebar/components/global_search/utils.js
@@ -1,5 +1,5 @@
import { pickBy } from 'lodash';
-import { truncateNamespace } from '~/lib/utils/text_utility';
+import { slugify, truncateNamespace } from '~/lib/utils/text_utility';
import {
GROUPS_CATEGORY,
PROJECTS_CATEGORY,
@@ -7,6 +7,7 @@ import {
ISSUES_CATEGORY,
RECENT_EPICS_CATEGORY,
} from '~/vue_shared/global_search/constants';
+import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from './command_palette/constants';
import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from './constants';
const getTruncatedNamespace = (string) => {
@@ -61,6 +62,15 @@ export const getFormattedItem = (item, searchContext) => {
const avatarSize = getAvatarSize(category);
const entityId = getEntityId(item, searchContext);
const entityName = getEntityName(item, searchContext);
+ const trackingLabel = slugify(category ?? '');
+ const trackingAttrs = trackingLabel
+ ? {
+ extraAttrs: {
+ 'data-track-action': TRACKING_CLICK_COMMAND_PALETTE_ITEM,
+ 'data-track-label': slugify(category, '_'),
+ },
+ }
+ : {};
return pickBy(
{
@@ -75,6 +85,7 @@ export const getFormattedItem = (item, searchContext) => {
namespace,
entity_id: entityId,
entity_name: entityName,
+ ...trackingAttrs,
},
(val) => val !== undefined,
);
diff --git a/app/assets/javascripts/super_sidebar/components/groups_list.vue b/app/assets/javascripts/super_sidebar/components/groups_list.vue
deleted file mode 100644
index 48becacebb7..00000000000
--- a/app/assets/javascripts/super_sidebar/components/groups_list.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import { MAX_FREQUENT_GROUPS_COUNT } from '../constants';
-import FrequentItemsList from './frequent_items_list.vue';
-import SearchResults from './search_results.vue';
-import NavItem from './nav_item.vue';
-
-export default {
- MAX_FREQUENT_GROUPS_COUNT,
- components: {
- FrequentItemsList,
- SearchResults,
- NavItem,
- },
- props: {
- username: {
- type: String,
- required: true,
- },
- viewAllLink: {
- type: String,
- required: true,
- },
- isSearch: {
- type: Boolean,
- required: false,
- default: false,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- storageKey() {
- return `${this.username}/frequent-groups`;
- },
- viewAllProps() {
- return {
- item: {
- link: this.viewAllLink,
- title: s__('Navigation|View all your groups'),
- icon: 'group',
- },
- linkClasses: { 'dashboard-shortcuts-groups': true },
- };
- },
- },
- i18n: {
- title: s__('Navigation|Frequently visited groups'),
- searchTitle: s__('Navigation|Groups'),
- pristineText: s__('Navigation|Groups you visit often will appear here.'),
- noResultsText: s__('Navigation|No group matches found'),
- },
-};
-</script>
-
-<template>
- <search-results
- v-if="isSearch"
- :title="$options.i18n.searchTitle"
- :no-results-text="$options.i18n.noResultsText"
- :search-results="searchResults"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </search-results>
- <frequent-items-list
- v-else
- :title="$options.i18n.title"
- :storage-key="storageKey"
- :max-items="$options.MAX_FREQUENT_GROUPS_COUNT"
- :pristine-text="$options.i18n.pristineText"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </frequent-items-list>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/items_list.vue b/app/assets/javascripts/super_sidebar/components/items_list.vue
deleted file mode 100644
index 1bad13f91e8..00000000000
--- a/app/assets/javascripts/super_sidebar/components/items_list.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<script>
-import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
-import NavItem from './nav_item.vue';
-
-export default {
- components: {
- ProjectAvatar,
- NavItem,
- },
- props: {
- items: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
-};
-</script>
-
-<template>
- <ul class="gl-p-0 gl-list-style-none">
- <nav-item
- v-for="item in items"
- :key="item.id"
- :item="item"
- is-subitem
- class="show-on-focus-or-hover--context"
- >
- <template #icon>
- <project-avatar
- :project-id="item.id"
- :project-name="item.title"
- :project-avatar-url="item.avatar"
- :size="24"
- aria-hidden="true"
- />
- </template>
- <template #actions>
- <slot name="actions" :item="item"></slot>
- </template>
- </nav-item>
- <slot name="view-all-items"></slot>
- </ul>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index d2d45ca7b6e..6b5002e1aa8 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -79,15 +79,26 @@ export default {
isExpanded(newIsExpanded) {
this.$emit('collapse-toggle', newIsExpanded);
this.keepFlyoutClosed = !this.newIsExpanded;
+ if (!newIsExpanded) {
+ this.isMouseOverFlyout = false;
+ }
},
},
methods: {
handlePointerover(e) {
+ if (!this.hasFlyout) return;
+
this.isMouseOverSection = e.pointerType === 'mouse';
},
handlePointerleave() {
- this.isMouseOverSection = false;
+ if (!this.hasFlyout) return;
+
this.keepFlyoutClosed = false;
+ // delay state change. otherwise the flyout menu gets removed before it
+ // has a chance to emit its mouseover event.
+ setTimeout(() => {
+ this.isMouseOverSection = false;
+ }, 5);
},
},
};
@@ -129,8 +140,7 @@ export default {
</button>
<flyout-menu
- v-if="hasFlyout"
- v-show="isMouseOver && !isExpanded && !keepFlyoutClosed"
+ v-if="hasFlyout && isMouseOver && !isExpanded && !keepFlyoutClosed && item.items.length > 0"
:target-id="`menu-section-button-${itemId}`"
:items="item.items"
@mouseover="isMouseOverFlyout = true"
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 36803a885e7..5e0f8fffb0e 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -1,6 +1,6 @@
<script>
-import { GlButton, GlIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlAvatar, GlButton, GlIcon, GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
import {
CLICK_MENU_ITEM_ACTION,
CLICK_PINNED_MENU_ITEM_ACTION,
@@ -12,11 +12,14 @@ import NavItemRouterLink from './nav_item_router_link.vue';
export default {
i18n: {
+ pin: s__('Navigation|Pin %{title}'),
pinItem: s__('Navigation|Pin item'),
+ unpin: s__('Navigation|Unpin %{title}'),
unpinItem: s__('Navigation|Unpin item'),
},
name: 'NavItem',
components: {
+ GlAvatar,
GlButton,
GlIcon,
GlBadge,
@@ -62,6 +65,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ isMouseIn: false,
+ canClickPinButton: false,
+ };
+ },
computed: {
pillData() {
return this.item.pill_count;
@@ -96,12 +105,27 @@ export default {
...extraData,
};
},
+ /**
+ * Some QA specs rely on a stable "Project overview"/"Group overview" nav
+ * item data-qa-submenu-item attribute value.
+ *
+ * This computed ensures that those particular nav items use the `id` of
+ * the item rather than its title for that QA attribute.
+ *
+ * In future, probably all nav items should do this, for consistency.
+ * See https://gitlab.com/gitlab-org/gitlab/-/issues/422925.
+ */
+ qaSubMenuItem() {
+ const { id } = this.item;
+ if (id === 'project_overview' || id === 'group_overview') return id.replace(/_/g, '-');
+ return this.item.title;
+ },
linkProps() {
return {
...this.$attrs,
...this.trackingProps,
item: this.item,
- 'data-qa-submenu-item': this.item.title,
+ 'data-qa-submenu-item': this.qaSubMenuItem,
'data-method': this.item.data_method ?? null,
};
},
@@ -118,26 +142,73 @@ export default {
navItemLinkComponent() {
return this.item.to ? NavItemRouterLink : NavItemLink;
},
+ hasAvatar() {
+ return Boolean(this.item.entity_id);
+ },
+ avatarShape() {
+ return this.item.avatar_shape || 'rect';
+ },
+ pinAriaLabel() {
+ return sprintf(this.$options.i18n.pin, {
+ title: this.item.title,
+ });
+ },
+ unpinAriaLabel() {
+ return sprintf(this.$options.i18n.unpin, {
+ title: this.item.title,
+ });
+ },
+ activeIndicatorStyle() {
+ const style = {
+ width: '3px',
+ borderRadius: '3px',
+ marginRight: '1px',
+ };
+
+ // The active indicator is too close to the avatar for items with one, so shift
+ // it left by 1px.
+ //
+ // The indicator is absolutely positioned using rem units. This tweak for this
+ // edge case is in pixel units, so that it does not scale with root font size.
+ if (this.hasAvatar) style.transform = 'translateX(-1px)';
+
+ return style;
+ },
+ },
+ mounted() {
+ if (this.item.is_active) {
+ this.$el.scrollIntoView(false);
+ }
+ },
+ methods: {
+ togglePointerEvents() {
+ this.canClickPinButton = this.isMouseIn;
+ },
},
};
</script>
<template>
- <li>
+ <li
+ class="gl-relative show-on-focus-or-hover--context hide-on-focus-or-hover--context transition-opacity-on-hover--context"
+ data-testid="nav-item"
+ @mouseenter="isMouseIn = true"
+ @mouseleave="isMouseIn = false"
+ >
<component
:is="navItemLinkComponent"
#default="{ isActive }"
v-bind="linkProps"
- class="nav-item-link 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--context"
+ 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-qa-selector="nav_item_link"
data-testid="nav-item-link"
+ data-qa-selector="nav_item_link"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
class="active-indicator gl-bg-blue-500 gl-absolute gl-left-2 gl-top-2 gl-bottom-2 gl-transition-slow"
aria-hidden="true"
- style="width: 3px; border-radius: 3px; margin-right: 1px"
+ :style="activeIndicatorStyle"
data-testid="active-indicator"
></div>
<div v-if="!isFlyout" class="gl-flex-shrink-0 gl-w-6 gl-display-flex">
@@ -148,6 +219,14 @@ export default {
name="grip"
class="gl-m-auto gl-text-gray-400 js-draggable-icon gl-cursor-grab show-on-focus-or-hover--target"
/>
+ <gl-avatar
+ v-else-if="hasAvatar"
+ :size="24"
+ :shape="avatarShape"
+ :entity-name="item.title"
+ :entity-id="item.entity_id"
+ :src="item.avatar"
+ />
</slot>
</div>
<div class="gl-flex-grow-1 gl-text-gray-900 gl-truncate-end">
@@ -157,36 +236,47 @@ export default {
</div>
</div>
<slot name="actions"></slot>
- <span v-if="hasPill || isPinnable" class="gl-text-right gl-relative">
+ <span v-if="hasPill || isPinnable" class="gl-text-right gl-relative gl-min-w-8">
<gl-badge
v-if="hasPill"
size="sm"
variant="neutral"
- :class="{ 'nav-item-badge gl-absolute gl-right-0 gl-top-2': isPinnable }"
+ class="gl-bg-t-gray-a-08!"
+ :class="{
+ 'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
+ }"
>
{{ pillData }}
</gl-badge>
- <gl-button
- v-if="isPinnable && !isPinned"
- v-gl-tooltip.noninteractive.ds500.right.viewport="$options.i18n.pinItem"
- size="small"
- category="tertiary"
- icon="thumbtack"
- class="show-on-focus-or-hover--target"
- :aria-label="$options.i18n.pinItem"
- @click.prevent="$emit('pin-add', item.id)"
- />
- <gl-button
- v-else-if="isPinnable && isPinned"
- v-gl-tooltip.noninteractive.ds500.right.viewport="$options.i18n.unpinItem"
- size="small"
- category="tertiary"
- :aria-label="$options.i18n.unpinItem"
- icon="thumbtack-solid"
- class="show-on-focus-or-hover--target"
- @click.prevent="$emit('pin-remove', item.id)"
- />
</span>
</component>
+ <template v-if="isPinnable">
+ <gl-button
+ v-if="isPinned"
+ v-gl-tooltip.noninteractive.right.viewport="$options.i18n.unpinItem"
+ :aria-label="unpinAriaLabel"
+ category="tertiary"
+ class="show-on-focus-or-hover--target transition-opacity-on-hover--target always-animate gl-absolute gl-right-3 gl-top-2"
+ :class="{ 'gl-pointer-events-none': !canClickPinButton }"
+ data-testid="nav-item-unpin"
+ icon="thumbtack-solid"
+ size="small"
+ @click="$emit('pin-remove', item.id)"
+ @transitionend="togglePointerEvents"
+ />
+ <gl-button
+ v-else
+ v-gl-tooltip.noninteractive.right.viewport="$options.i18n.pinItem"
+ :aria-label="pinAriaLabel"
+ category="tertiary"
+ class="show-on-focus-or-hover--target transition-opacity-on-hover--target always-animate gl-absolute gl-right-3 gl-top-2"
+ :class="{ 'gl-pointer-events-none': !canClickPinButton }"
+ data-testid="nav-item-pin"
+ icon="thumbtack"
+ size="small"
+ @click="$emit('pin-add', item.id)"
+ @transitionend="togglePointerEvents"
+ />
+ </template>
</li>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 1e2201fbdff..5da45b52bf4 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -6,6 +6,13 @@ import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '../cons
import MenuSection from './menu_section.vue';
import NavItem from './nav_item.vue';
+const AMBIGUOUS_SETTINGS = {
+ ci_cd: s__('Navigation|CI/CD settings'),
+ merge_request_settings: s__('Navigation|Merge requests settings'),
+ monitor: s__('Navigation|Monitor settings'),
+ repository: s__('Navigation|Repository settings'),
+};
+
export default {
i18n: {
pinned: s__('Navigation|Pinned'),
@@ -23,11 +30,6 @@ export default {
required: false,
default: () => [],
},
- separated: {
- type: Boolean,
- required: false,
- default: false,
- },
hasFlyout: {
type: Boolean,
required: false,
@@ -37,7 +39,7 @@ export default {
data() {
return {
expanded: getCookie(SIDEBAR_PINS_EXPANDED_COOKIE) !== 'false',
- draggableItems: this.items,
+ draggableItems: this.renameSettings(this.items),
};
},
computed: {
@@ -63,7 +65,7 @@ export default {
});
},
items(newItems) {
- this.draggableItems = newItems;
+ this.draggableItems = this.renameSettings(newItems);
},
},
methods: {
@@ -76,6 +78,15 @@ export default {
event.oldIndex < event.newIndex,
);
},
+ renameSettings(items) {
+ return items.map((i) => {
+ const title = AMBIGUOUS_SETTINGS[i.id] || i.title;
+ return { ...i, title };
+ });
+ },
+ onPinRemove(itemId) {
+ this.$emit('pin-remove', itemId);
+ },
},
};
</script>
@@ -84,10 +95,9 @@ export default {
<menu-section
:item="sectionItem"
:expanded="expanded"
- :separated="separated"
:has-flyout="hasFlyout"
@collapse-toggle="expanded = !expanded"
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-remove="onPinRemove"
>
<draggable
v-if="items.length > 0"
@@ -103,7 +113,7 @@ export default {
:key="item.id"
:item="item"
is-in-pinned-section
- @pin-remove="(itemId) => $emit('pin-remove', itemId)"
+ @pin-remove="onPinRemove"
/>
</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/projects_list.vue b/app/assets/javascripts/super_sidebar/components/projects_list.vue
deleted file mode 100644
index 8d1a5c825b5..00000000000
--- a/app/assets/javascripts/super_sidebar/components/projects_list.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import { MAX_FREQUENT_PROJECTS_COUNT } from '../constants';
-import FrequentItemsList from './frequent_items_list.vue';
-import SearchResults from './search_results.vue';
-import NavItem from './nav_item.vue';
-
-export default {
- MAX_FREQUENT_PROJECTS_COUNT,
- components: {
- FrequentItemsList,
- SearchResults,
- NavItem,
- },
- props: {
- username: {
- type: String,
- required: true,
- },
- viewAllLink: {
- type: String,
- required: true,
- },
- isSearch: {
- type: Boolean,
- required: false,
- default: false,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- storageKey() {
- return `${this.username}/frequent-projects`;
- },
- viewAllProps() {
- return {
- item: {
- link: this.viewAllLink,
- title: s__('Navigation|View all your projects'),
- icon: 'project',
- },
- linkClasses: { 'dashboard-shortcuts-projects': true },
- };
- },
- },
- i18n: {
- title: s__('Navigation|Frequently visited projects'),
- searchTitle: s__('Navigation|Projects'),
- pristineText: s__('Navigation|Projects you visit often will appear here.'),
- noResultsText: s__('Navigation|No project matches found'),
- },
-};
-</script>
-
-<template>
- <search-results
- v-if="isSearch"
- class="gl-border-t-0"
- :title="$options.i18n.searchTitle"
- :no-results-text="$options.i18n.noResultsText"
- :search-results="searchResults"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </search-results>
- <frequent-items-list
- v-else
- :title="$options.i18n.title"
- :storage-key="storageKey"
- :max-items="$options.MAX_FREQUENT_PROJECTS_COUNT"
- :pristine-text="$options.i18n.pristineText"
- >
- <template #view-all-items>
- <nav-item v-bind="viewAllProps" is-subitem />
- </template>
- </frequent-items-list>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/search_results.vue b/app/assets/javascripts/super_sidebar/components/search_results.vue
deleted file mode 100644
index ff933f341af..00000000000
--- a/app/assets/javascripts/super_sidebar/components/search_results.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<script>
-import { GlCollapse, GlCollapseToggleDirective, GlIcon } from '@gitlab/ui';
-import uniqueId from 'lodash/uniqueId';
-import ItemsList from './items_list.vue';
-
-export default {
- components: {
- GlCollapse,
- GlIcon,
- ItemsList,
- },
- directives: {
- CollapseToggle: GlCollapseToggleDirective,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- noResultsText: {
- type: String,
- required: true,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- expanded: true,
- };
- },
- computed: {
- isEmpty() {
- return !this.searchResults.length;
- },
- collapseIcon() {
- return this.expanded ? 'chevron-up' : 'chevron-down';
- },
- },
- created() {
- this.collapseId = uniqueId('expandable-section-');
- },
- buttonClasses: [
- // Reset user agent styles
- 'gl-appearance-none',
- 'gl-border-0',
- 'gl-bg-transparent',
- // Text styles
- 'gl-text-left',
- 'gl-text-transform-uppercase',
- 'gl-text-secondary',
- 'gl-font-weight-semibold',
- 'gl-font-xs',
- 'gl-line-height-12',
- 'gl-letter-spacing-06em',
- // Border
- 'gl-border-t',
- 'gl-border-gray-50',
- // Spacing
- 'gl-my-3',
- 'gl-pt-2',
- // Layout
- 'gl-display-flex',
- 'gl-justify-content-space-between',
- 'gl-align-items-center',
- ],
-};
-</script>
-
-<template>
- <li class="gl-border-t gl-border-gray-50">
- <button
- v-collapse-toggle="collapseId"
- :class="$options.buttonClasses"
- class="gl-mx-3"
- data-testid="search-results-toggle"
- >
- {{ title }}
- <gl-icon :name="collapseIcon" :size="16" />
- </button>
- <gl-collapse :id="collapseId" v-model="expanded">
- <div
- v-if="isEmpty"
- data-testid="empty-text"
- class="gl-text-gray-500 gl-font-sm gl-mb-3 gl-mx-4"
- >
- {{ noResultsText }}
- </div>
- <items-list :aria-label="title" :items="searchResults">
- <template #view-all-items>
- <slot name="view-all-items"></slot>
- </template>
- </items-list>
- </gl-collapse>
- </li>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue
new file mode 100644
index 00000000000..df432a1928a
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_hover_peek_behavior.vue
@@ -0,0 +1,126 @@
+<script>
+import { getCssClassDimensions } from '~/lib/utils/css_utils';
+import Tracking from '~/tracking';
+import {
+ JS_TOGGLE_EXPAND_CLASS,
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '../constants';
+
+export default {
+ name: 'SidebarHoverPeek',
+ mixins: [Tracking.mixin()],
+ props: {
+ isMouseOverSidebar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ created() {
+ // Nothing needs to observe these properties, so they are not reactive.
+ this.state = null;
+ this.openTimer = null;
+ this.closeTimer = null;
+ this.xSidebarEdge = null;
+ this.isMouseWithinSidebarArea = false;
+ },
+ async mounted() {
+ await this.$nextTick();
+ this.xSidebarEdge = getCssClassDimensions('super-sidebar').width;
+ document.addEventListener('mousemove', this.onMouseMove);
+ document.documentElement.addEventListener('mouseleave', this.onDocumentLeave);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .addEventListener('mouseenter', this.onMouseEnter);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .addEventListener('mouseleave', this.onMouseLeave);
+ this.changeState(STATE_CLOSED);
+ },
+ beforeDestroy() {
+ document.removeEventListener('mousemove', this.onMouseMove);
+ document.documentElement.removeEventListener('mouseleave', this.onDocumentLeave);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .removeEventListener('mouseenter', this.onMouseEnter);
+ document
+ .querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`)
+ .removeEventListener('mouseleave', this.onMouseLeave);
+ this.clearTimers();
+ },
+ methods: {
+ onMouseMove({ clientX }) {
+ if (clientX < this.xSidebarEdge) {
+ this.isMouseWithinSidebarArea = true;
+ } else {
+ this.isMouseWithinSidebarArea = false;
+ if (!this.isMouseOverSidebar && this.state === STATE_OPEN) {
+ this.willClose();
+ }
+ }
+ },
+ onDocumentLeave() {
+ this.isMouseWithinSidebarArea = false;
+ if (this.state === STATE_OPEN) {
+ this.willClose();
+ } else if (this.state === STATE_WILL_OPEN) {
+ this.close();
+ }
+ },
+ onMouseEnter() {
+ clearTimeout(this.closeTimer);
+ this.willOpen();
+ },
+ onMouseLeave() {
+ clearTimeout(this.openTimer);
+ if (this.isMouseWithinSidebarArea || this.isMouseOverSidebar) return;
+ this.willClose();
+ },
+ willClose() {
+ this.changeState(STATE_WILL_CLOSE);
+ this.closeTimer = setTimeout(this.close, SUPER_SIDEBAR_PEEK_CLOSE_DELAY);
+ },
+ willOpen() {
+ this.changeState(STATE_WILL_OPEN);
+ this.openTimer = setTimeout(this.open, SUPER_SIDEBAR_PEEK_OPEN_DELAY);
+ },
+ open() {
+ this.changeState(STATE_OPEN);
+ this.clearTimers();
+ this.track('nav_hover_peek', {
+ label: 'nav_sidebar_toggle',
+ property: 'nav_sidebar',
+ });
+ },
+ close() {
+ if (this.isMouseWithinSidebarArea) return;
+ this.changeState(STATE_CLOSED);
+ this.clearTimers();
+ },
+ clearTimers() {
+ clearTimeout(this.closeTimer);
+ clearTimeout(this.openTimer);
+ },
+ /**
+ * Switches to the new state, and emits a change event.
+ *
+ * If the given state is the current state, do nothing.
+ *
+ * @param {string} state The state to transition to.
+ */
+ changeState(state) {
+ if (this.state === state) return;
+ this.state = state;
+ this.$emit('change', state);
+ },
+ },
+ render() {
+ return null;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index 821b9dbcb7b..02488e99c0e 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -2,7 +2,6 @@
import * as Sentry from '@sentry/browser';
import { GlBreakpointInstance, breakpoints } from '@gitlab/ui/dist/utils';
import axios from '~/lib/utils/axios_utils';
-import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PANELS_WITH_PINS } from '../constants';
import NavItem from './nav_item.vue';
@@ -51,10 +50,6 @@ export default {
},
},
- i18n: {
- mainNavigation: s__('Navigation|Main navigation'),
- },
-
data() {
return {
showFlyoutMenus: false,
@@ -109,10 +104,8 @@ export default {
},
},
mounted() {
- if (this.glFeatures.superSidebarFlyoutMenus) {
- this.decideFlyoutState();
- window.addEventListener('resize', this.decideFlyoutState);
- }
+ this.decideFlyoutState();
+ window.addEventListener('resize', this.decideFlyoutState);
},
beforeDestroy() {
window.removeEventListener('resize', this.decideFlyoutState);
@@ -164,13 +157,12 @@ export default {
</script>
<template>
- <nav :aria-label="$options.i18n.mainNavigation" class="gl-p-2 gl-relative">
+ <div class="gl-p-2 gl-relative">
<ul v-if="hasStaticItems" class="gl-p-0 gl-m-0" data-testid="static-items-section">
<nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static />
</ul>
<pinned-section
v-if="supportsPins"
- separated
:items="pinnedItems"
:has-flyout="showFlyoutMenus"
@pin-remove="destroyPin"
@@ -203,5 +195,5 @@ export default {
/>
</template>
</ul>
- </nav>
+ </div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
index ec728b4af9e..a20e37b945a 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_peek_behavior.vue
@@ -1,12 +1,14 @@
<script>
import { getCssClassDimensions } from '~/lib/utils/css_utils';
import Tracking from '~/tracking';
-import { SUPER_SIDEBAR_PEEK_OPEN_DELAY, SUPER_SIDEBAR_PEEK_CLOSE_DELAY } from '../constants';
-
-export const STATE_CLOSED = 'closed';
-export const STATE_WILL_OPEN = 'will-open';
-export const STATE_OPEN = 'open';
-export const STATE_WILL_CLOSE = 'will-close';
+import {
+ SUPER_SIDEBAR_PEEK_OPEN_DELAY,
+ SUPER_SIDEBAR_PEEK_CLOSE_DELAY,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE as STATE_WILL_CLOSE,
+} from '../constants';
export default {
name: 'SidebarPeek',
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 29a3147e949..fe3e4a8199e 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -2,27 +2,31 @@
import { GlButton } from '@gitlab/ui';
import { Mousetrap } from '~/lib/mousetrap';
import { keysFor, TOGGLE_SUPER_SIDEBAR } from '~/behaviors/shortcuts/keybindings';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
-import { sidebarState } from '../constants';
+import {
+ sidebarState,
+ SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
+ SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN as STATE_WILL_OPEN,
+ SUPER_SIDEBAR_PEEK_STATE_OPEN as STATE_OPEN,
+} from '../constants';
import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager';
+import { trackContextAccess } from '../utils';
import UserBar from './user_bar.vue';
import SidebarPortalTarget from './sidebar_portal_target.vue';
-import ContextHeader from './context_header.vue';
-import ContextSwitcher from './context_switcher.vue';
import HelpCenter from './help_center.vue';
import SidebarMenu from './sidebar_menu.vue';
-import SidebarPeekBehavior, { STATE_CLOSED, STATE_WILL_OPEN } from './sidebar_peek_behavior.vue';
+import SidebarPeekBehavior from './sidebar_peek_behavior.vue';
+import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue';
export default {
components: {
GlButton,
UserBar,
- ContextHeader,
- ContextSwitcher,
HelpCenter,
SidebarMenu,
SidebarPeekBehavior,
+ SidebarHoverPeekBehavior,
SidebarPortalTarget,
TrialStatusWidget: () =>
import('ee_component/contextual_sidebar/components/trial_status_widget.vue'),
@@ -32,6 +36,7 @@ export default {
mixins: [Tracking.mixin()],
i18n: {
skipToMainContent: __('Skip to main content'),
+ primary: s__('Navigation|Primary'),
},
inject: ['showTrialStatusWidget'],
props: {
@@ -45,25 +50,34 @@ export default {
sidebarState,
showPeekHint: false,
isMouseover: false,
+ breakpoint: null,
};
},
computed: {
+ showOverlay() {
+ return this.sidebarState.isPeek || this.sidebarState.isHoverPeek;
+ },
menuItems() {
return this.sidebarData.current_menu_items || [];
},
peekClasses() {
return {
'super-sidebar-peek-hint': this.showPeekHint,
- 'super-sidebar-peek': this.sidebarState.isPeek,
+ 'super-sidebar-peek': this.showOverlay,
+ 'super-sidebar-has-peeked': this.sidebarState.hasPeeked,
};
},
},
- watch: {
- 'sidebarState.isCollapsed': function isCollapsedWatcher(newIsCollapsed) {
- if (newIsCollapsed && this.$refs['context-switcher']) {
- this.$refs['context-switcher'].close();
- }
- },
+ created() {
+ const {
+ is_logged_in: isLoggedIn,
+ current_context: currentContext,
+ username,
+ track_visits_path: trackVisitsPath,
+ } = this.sidebarData;
+ if (isLoggedIn && currentContext.namespace) {
+ trackContextAccess(username, currentContext, trackVisitsPath);
+ }
},
mounted() {
Mousetrap.bind(keysFor(TOGGLE_SUPER_SIDEBAR), this.toggleSidebar);
@@ -88,6 +102,7 @@ export default {
this.sidebarState.isCollapsed = true;
this.showPeekHint = false;
} else if (state === STATE_WILL_OPEN) {
+ this.sidebarState.hasPeeked = true;
this.sidebarState.isPeek = false;
this.sidebarState.isCollapsed = true;
this.showPeekHint = true;
@@ -97,8 +112,15 @@ export default {
this.showPeekHint = false;
}
},
- onContextSwitcherToggled(open) {
- this.sidebarState.contextSwitcherOpen = open;
+ onHoverPeekChange(state) {
+ if (state === STATE_OPEN) {
+ this.sidebarState.hasPeeked = true;
+ this.sidebarState.isHoverPeek = true;
+ this.sidebarState.isCollapsed = false;
+ } else if (state === STATE_CLOSED) {
+ this.sidebarState.isHoverPeek = false;
+ this.sidebarState.isCollapsed = true;
+ }
},
},
};
@@ -114,8 +136,9 @@ export default {
>
{{ $options.i18n.skipToMainContent }}
</gl-button>
- <aside
+ <nav
id="super-sidebar"
+ :aria-label="$options.i18n.primary"
class="super-sidebar"
:class="peekClasses"
data-testid="super-sidebar"
@@ -124,32 +147,23 @@ export default {
@mouseenter="isMouseover = true"
@mouseleave="isMouseover = false"
>
- <user-bar :has-collapse-button="!sidebarState.isPeek" :sidebar-data="sidebarData" />
+ <user-bar :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" />
<div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2">
<trial-status-widget
- class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! nav-item-link gl-py-3"
+ class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-py-3"
/>
<trial-status-popover />
</div>
<div
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"
>
- <div
- class="gl-flex-grow-1"
- :class="{ 'gl-overflow-auto': !sidebarState.contextSwitcherOpen }"
- data-testid="nav-container"
- >
- <context-switcher
- v-if="sidebarData.is_logged_in"
- ref="context-switcher"
- :username="sidebarData.username"
- :projects-path="sidebarData.projects_path"
- :groups-path="sidebarData.groups_path"
- :current-context="sidebarData.current_context"
- :context-header="sidebarData.current_context_header"
- @toggle="onContextSwitcherToggled"
- />
- <context-header v-else :context="sidebarData.current_context_header" />
+ <div class="gl-flex-grow-1 gl-overflow-auto" data-testid="nav-container">
+ <h2
+ class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-sm super-sidebar-context-header"
+ >
+ {{ sidebarData.current_context_header }}
+ </h2>
+
<sidebar-menu
v-if="menuItems.length"
:items="menuItems"
@@ -164,7 +178,7 @@ export default {
<help-center :sidebar-data="sidebarData" />
</div>
</div>
- </aside>
+ </nav>
<a
v-for="shortcutLink in sidebarData.shortcut_links"
:key="shortcutLink.href"
@@ -176,13 +190,18 @@ export default {
</a>
<!--
- Only mount SidebarPeekBehavior if the sidebar is peekable, to avoid
+ Only mount peek behavior components if the sidebar is peekable, to avoid
setting up event listeners unnecessarily.
-->
<sidebar-peek-behavior
- v-if="sidebarState.isPeekable"
+ v-if="sidebarState.isPeekable && !sidebarState.isHoverPeek"
:is-mouse-over-sidebar="isMouseover"
@change="onPeekChange"
/>
+ <sidebar-hover-peek-behavior
+ v-if="sidebarState.isPeekable && !sidebarState.isPeek"
+ :is-mouse-over-sidebar="isMouseover"
+ @change="onHoverPeekChange"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
index 7d5e87805d5..30ee18cc369 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
@@ -27,19 +27,18 @@ export default {
},
i18n: {
collapseSidebar: __('Hide sidebar'),
- expandSidebar: __('Show sidebar'),
- navigationSidebar: __('Navigation sidebar'),
+ expandSidebar: __('Keep sidebar visible'),
+ primaryNavigationSidebar: __('Primary navigation sidebar'),
},
data() {
return sidebarState;
},
computed: {
+ canOpen() {
+ return this.isCollapsed || this.isPeek || this.isHoverPeek;
+ },
tooltipTitle() {
- if (this.isPeek) return '';
-
- return this.isCollapsed
- ? this.$options.i18n.expandSidebar
- : this.$options.i18n.collapseSidebar;
+ return this.canOpen ? this.$options.i18n.expandSidebar : this.$options.i18n.collapseSidebar;
},
tooltip() {
return {
@@ -49,21 +48,21 @@ export default {
};
},
ariaExpanded() {
- return String(!this.isCollapsed);
+ return String(!this.canOpen);
},
},
methods: {
toggle() {
- this.track(this.isCollapsed ? 'nav_show' : 'nav_hide', {
+ this.track(this.canOpen ? 'nav_show' : 'nav_hide', {
label: 'nav_toggle',
property: 'nav_sidebar',
});
- toggleSuperSidebarCollapsed(!this.isCollapsed, true);
+ toggleSuperSidebarCollapsed(!this.canOpen, true);
this.focusOtherToggle();
},
focusOtherToggle() {
this.$nextTick(() => {
- const classSelector = this.isCollapsed ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS;
+ const classSelector = this.canOpen ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS;
const otherToggle = document.querySelector(`.${classSelector}`);
otherToggle?.focus();
});
@@ -74,13 +73,12 @@ export default {
<template>
<gl-button
- v-gl-tooltip.hover.noninteractive.ds500="tooltip"
+ v-gl-tooltip.hover="tooltip"
aria-controls="super-sidebar"
:aria-expanded="ariaExpanded"
- :aria-label="$options.i18n.navigationSidebar"
+ :aria-label="$options.i18n.primaryNavigationSidebar"
icon="sidebar"
category="tertiary"
- :disabled="isPeek"
@click="toggle"
/>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index b76ef91b768..49aee4f3470 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -1,5 +1,5 @@
<script>
-import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlButton, GlModalDirective, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import {
destroyUserCountsManager,
@@ -34,20 +34,19 @@ export default {
),
SuperSidebarToggle,
BrandLogo,
+ GlIcon,
},
i18n: {
- createNew: __('Create new...'),
- homepage: __('Homepage'),
issues: __('Issues'),
mergeRequests: __('Merge requests'),
- search: __('Search'),
searchKbdHelp: sprintf(
- s__('GlobalSearch|Search GitLab %{kbdOpen}/%{kbdClose}'),
+ s__('GlobalSearch|Type %{kbdOpen}/%{kbdClose} to search'),
{ kbdOpen: '<kbd>', kbdClose: '</kbd>' },
false,
),
todoList: __('To-Do list'),
stopImpersonating: __('Stop impersonating'),
+ searchBtnText: __('Search or go to…'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -103,8 +102,14 @@ export default {
</script>
<template>
- <div class="user-bar">
- <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2">
+ <div
+ class="user-bar gl-display-flex gl-p-3 gl-gap-1"
+ :class="{ 'gl-flex-direction-column gl-gap-3': sidebarData.is_logged_in }"
+ >
+ <div
+ v-if="hasCollapseButton || sidebarData.is_logged_in"
+ class="gl-display-flex gl-align-items-center gl-gap-1"
+ >
<template v-if="sidebarData.is_logged_in">
<brand-logo :logo-url="sidebarData.logo_url" />
<gl-badge
@@ -112,7 +117,6 @@ export default {
variant="success"
:href="sidebarData.canary_toggle_com_url"
size="sm"
- class="gl-ml-2"
>
{{ $options.NEXT_LABEL }}
</gl-badge>
@@ -126,24 +130,16 @@ export default {
tooltip-container="super-sidebar"
data-testid="super-sidebar-collapse-button"
/>
- <create-menu v-if="sidebarData.is_logged_in" :groups="sidebarData.create_new_menu_groups" />
-
- <gl-button
- id="super-sidebar-search"
- v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
- v-gl-modal="$options.SEARCH_MODAL_ID"
- data-testid="super-sidebar-search-button"
- icon="search"
- :aria-label="$options.i18n.search"
- category="tertiary"
+ <create-menu
+ v-if="sidebarData.is_logged_in && sidebarData.create_new_menu_groups.length > 0"
+ :groups="sidebarData.create_new_menu_groups"
/>
- <search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
<user-menu v-if="sidebarData.is_logged_in" :data="sidebarData" />
<gl-button
v-if="isImpersonating"
- v-gl-tooltip.noninteractive.ds500.bottom
+ v-gl-tooltip.bottom
:href="sidebarData.stop_impersonation_path"
:title="$options.i18n.stopImpersonating"
:aria-label="$options.i18n.stopImpersonating"
@@ -155,10 +151,10 @@ export default {
</div>
<div
v-if="sidebarData.is_logged_in"
- class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2"
+ class="gl-display-flex gl-justify-content-space-between gl-gap-2"
>
<counter
- v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.issues"
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
class="gl-flex-basis-third dashboard-shortcuts-issues"
icon="issues"
:count="userCounts.assigned_issues"
@@ -176,9 +172,7 @@ export default {
@hidden="mrMenuShown = false"
>
<counter
- v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="
- mrMenuShown ? '' : $options.i18n.mergeRequests
- "
+ v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
class="gl-w-full"
icon="merge-request-open"
:count="mergeRequestTotalCount"
@@ -190,7 +184,7 @@ export default {
/>
</merge-request-menu>
<counter
- v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.todoList"
+ v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
:count="userCounts.todos"
@@ -202,5 +196,16 @@ export default {
data-track-property="nav_core_menu"
/>
</div>
+ <button
+ id="super-sidebar-search"
+ v-gl-tooltip.bottom.hover.html="searchTooltip"
+ v-gl-modal="$options.SEARCH_MODAL_ID"
+ class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
+ data-testid="super-sidebar-search-button"
+ >
+ <gl-icon name="search" />
+ {{ $options.i18n.searchBtnText }}
+ </button>
+ <search-modal @shown="hideSearchTooltip" @hidden="showSearchTooltip" />
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 869f07520a2..ed6c41e85c6 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -19,7 +19,6 @@ const DROPDOWN_X_OFFSET_BASE = -211;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
- feedbackUrl: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409005',
i18n: {
newNavigation: {
sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
@@ -31,7 +30,6 @@ export default {
buyPipelineMinutes: s__('CurrentUser|Buy Pipeline minutes'),
oneOfGroupsRunningOutOfPipelineMinutes: s__('CurrentUser|One of your groups is running out'),
gitlabNext: s__('CurrentUser|Switch to GitLab Next'),
- provideFeedback: s__('NorthstarNavigation|Provide feedback'),
startTrial: s__('CurrentUser|Start an Ultimate trial'),
signOut: __('Sign out'),
},
@@ -131,17 +129,6 @@ export default {
},
};
},
- feedbackItem() {
- return {
- text: this.$options.i18n.provideFeedback,
- href: this.$options.feedbackUrl,
- extraAttrs: {
- target: '_blank',
- ...USER_MENU_TRACKING_DEFAULTS,
- 'data-track-label': 'provide_nav_feedback',
- },
- };
- },
signOutGroup() {
return {
items: [
@@ -316,7 +303,6 @@ export default {
<span class="gl-font-sm">{{ $options.i18n.newNavigation.sectionTitle }}</span>
</template>
<new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled new-navigation />
- <gl-disclosure-dropdown-item :item="feedbackItem" data-testid="feedback-item" />
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
diff --git a/app/assets/javascripts/super_sidebar/components/user_name_group.vue b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
index 13f19338610..3c8059387fa 100644
--- a/app/assets/javascripts/super_sidebar/components/user_name_group.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_name_group.vue
@@ -71,7 +71,7 @@ export default {
v-if="user.status.customized"
ref="statusTooltipTarget"
data-testid="user-menu-status"
- class="gl-display-flex gl-align-items-center gl-mt-2 gl-font-sm"
+ class="gl-display-flex gl-align-items-baseline gl-mt-2 gl-font-sm"
>
<gl-emoji :data-name="user.status.emoji" class="gl-mr-1" />
<span v-safe-html="user.status.message_html" class="gl-text-truncate"></span>
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index 757bf9c7459..77bd8b4a734 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -13,10 +13,12 @@ export const portalState = Vue.observable({
});
export const sidebarState = Vue.observable({
- contextSwitcherOpen: false,
isCollapsed: false,
+ hasPeeked: false,
isPeek: false,
isPeekable: false,
+ isHoverPeek: false,
+ wasHoverPeek: false,
});
export const helpCenterState = Vue.observable({
@@ -28,13 +30,17 @@ export const MAX_FREQUENT_GROUPS_COUNT = 3;
export const SUPER_SIDEBAR_PEEK_OPEN_DELAY = 200;
export const SUPER_SIDEBAR_PEEK_CLOSE_DELAY = 500;
+export const SUPER_SIDEBAR_PEEK_STATE_CLOSED = 'closed';
+export const SUPER_SIDEBAR_PEEK_STATE_WILL_OPEN = 'will-open';
+export const SUPER_SIDEBAR_PEEK_STATE_OPEN = 'open';
+export const SUPER_SIDEBAR_PEEK_STATE_WILL_CLOSE = 'will-close';
export const TRACKING_UNKNOWN_ID = 'item_without_id';
export const TRACKING_UNKNOWN_PANEL = 'nav_panel_unknown';
export const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
export const CLICK_PINNED_MENU_ITEM_ACTION = 'click_pinned_menu_item';
-export const PANELS_WITH_PINS = ['group', 'project'];
+export const PANELS_WITH_PINS = ['group', 'project', 'organization'];
export const USER_MENU_TRACKING_DEFAULTS = {
'data-track-property': 'nav_user_menu',
diff --git a/app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql b/app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql
deleted file mode 100644
index 4b1e65be3fa..00000000000
--- a/app/assets/javascripts/super_sidebar/graphql/queries/search_user_groups_and_projects.query.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-query searchUserProjectsAndGroups($username: String!, $search: String) {
- projects(search: $search, sort: "latest_activity_desc", membership: true, first: 20) {
- nodes {
- id
- name
- namespace: nameWithNamespace
- webUrl
- avatarUrl
- }
- }
-
- user(username: $username) {
- id
- groups(search: $search, first: 20) {
- nodes {
- id
- name
- namespace: fullPath
- webUrl
- avatarUrl
- }
- }
- }
-}
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index 2b62e7a6ede..de16161efb5 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,6 +1,4 @@
import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { initStatusTriggers } from '../header';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
@@ -12,12 +10,6 @@ import {
import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
-Vue.use(VueApollo);
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
const getTrialStatusWidgetData = (sidebarData) => {
if (sidebarData.trial_status_widget_data_attrs && sidebarData.trial_status_popover_data_attrs) {
const {
@@ -97,7 +89,6 @@ export const initSuperSidebar = () => {
return new Vue({
el,
name: 'SuperSidebarRoot',
- apolloProvider,
provide: {
rootPath,
toggleNewNavEndpoint,
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
index feb7e274b07..9ee78a657b6 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js
@@ -26,6 +26,9 @@ export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => {
sidebarState.isPeek = false;
sidebarState.isPeekable = collapsed;
+ sidebarState.hasPeeked = false;
+ sidebarState.isHoverPeek = false;
+ sidebarState.wasHoverPeek = false;
sidebarState.isCollapsed = collapsed;
if (saveCookie && isDesktopBreakpoint()) {
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index cbf93155fb6..97830a32d78 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/browser';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
-import { truncateNamespace } from '~/lib/utils/text_utility';
+import axios from '~/lib/utils/axios_utils';
/**
* This takes an array of project or groups that were stored in the local storage, to be shown in
@@ -18,7 +18,8 @@ const sortItemsByFrequencyAndLastAccess = (items) =>
// and then by lastAccessedOn with recent most first
if (itemA.frequency !== itemB.frequency) {
return itemB.frequency - itemA.frequency;
- } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ }
+ if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
return itemB.lastAccessedOn - itemA.lastAccessedOn;
}
@@ -36,11 +37,43 @@ export const getTopFrequentItems = (items, maxCount) => {
return frequentItems.slice(0, maxCount);
};
-const updateItemAccess = (contextItem, { lastAccessedOn, frequency = 0 } = {}) => {
+/**
+ * This tracks projects' and groups' visits in order to suggest a list of frequently visited
+ * entities to the user. Currently, this track visits in two ways:
+ * - The legacy approach uses a simple counting algorithm and stores the data in the local storage.
+ * - The above approach is being migrated to a backend-based one, where visits will be stored in the
+ * DB, and suggestions will be made through a smarter algorithm. When we are ready to transition
+ * to the newer approach, the legacy one will be cleaned up.
+ * @param {object} item The project/group item being tracked.
+ * @param {string} namespace A string indicating whether the tracked entity is a project or a group.
+ * @param {string} trackVisitsPath The API endpoint to track visits server-side.
+ * @returns {void}
+ */
+const updateItemAccess = (
+ contextItem,
+ { lastAccessedOn, frequency = 0 } = {},
+ namespace,
+ trackVisitsPath,
+) => {
const now = Date.now();
const neverAccessed = !lastAccessedOn;
const shouldUpdate = neverAccessed || Math.abs(now - lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1;
+ if (shouldUpdate && gon.features?.serverSideFrecentNamespaces) {
+ try {
+ axios({
+ url: trackVisitsPath,
+ method: 'POST',
+ data: {
+ type: namespace,
+ id: contextItem.id,
+ },
+ });
+ } catch (e) {
+ Sentry.captureException(e);
+ }
+ }
+
return {
...contextItem,
frequency: shouldUpdate ? frequency + 1 : frequency,
@@ -48,7 +81,7 @@ const updateItemAccess = (contextItem, { lastAccessedOn, frequency = 0 } = {}) =
};
};
-export const trackContextAccess = (username, context) => {
+export const trackContextAccess = (username, context, trackVisitsPath) => {
if (!AccessorUtilities.canUseLocalStorage()) {
return false;
}
@@ -61,9 +94,19 @@ export const trackContextAccess = (username, context) => {
);
if (existingItemIndex > -1) {
- storedItems[existingItemIndex] = updateItemAccess(context.item, storedItems[existingItemIndex]);
+ storedItems[existingItemIndex] = updateItemAccess(
+ context.item,
+ storedItems[existingItemIndex],
+ context.namespace,
+ trackVisitsPath,
+ );
} else {
- const newItem = updateItemAccess(context.item);
+ const newItem = updateItemAccess(
+ context.item,
+ storedItems[existingItemIndex],
+ context.namespace,
+ trackVisitsPath,
+ );
if (storedItems.length === FREQUENT_ITEMS.MAX_COUNT) {
sortItemsByFrequencyAndLastAccess(storedItems);
storedItems.pop();
@@ -74,15 +117,6 @@ export const trackContextAccess = (username, context) => {
return localStorage.setItem(storageKey, JSON.stringify(storedItems));
};
-export const formatContextSwitcherItems = (items) =>
- items.map(({ id, name: title, namespace, avatarUrl: avatar, webUrl: link }) => ({
- id,
- title,
- subtitle: truncateNamespace(namespace),
- avatar,
- link,
- }));
-
export const getItemsFromLocalStorage = ({ storageKey, maxItems }) => {
if (!AccessorUtilities.canUseLocalStorage()) {
return [];
diff --git a/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql b/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql
index 3ba0ab29530..0de76e25d01 100644
--- a/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql
+++ b/app/assets/javascripts/time_tracking/components/queries/get_timelogs.query.graphql
@@ -1,8 +1,8 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query timeTrackingReport(
- $startDate: Time
- $endDate: Time
+ $startTime: Time
+ $endTime: Time
$projectId: ProjectID
$groupId: GroupID
$username: String
@@ -12,8 +12,8 @@ query timeTrackingReport(
$after: String
) {
timelogs(
- startDate: $startDate
- endDate: $endDate
+ startTime: $startTime
+ endTime: $endTime
projectId: $projectId
groupId: $groupId
username: $username
diff --git a/app/assets/javascripts/time_tracking/components/timelogs_app.vue b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
index 2069e4a6722..7bb9b6c52a5 100644
--- a/app/assets/javascripts/time_tracking/components/timelogs_app.vue
+++ b/app/assets/javascripts/time_tracking/components/timelogs_app.vue
@@ -17,11 +17,11 @@ import TimelogsTable from './timelogs_table.vue';
const ENTRIES_PER_PAGE = 20;
// Define initial dates to current date and time
-const INITIAL_TO_DATE = new Date();
-const INITIAL_FROM_DATE = new Date();
+const INITIAL_TO_DATE_TIME = new Date(new Date().setHours(0, 0, 0, 0));
+const INITIAL_FROM_DATE_TIME = new Date(new Date().setHours(0, 0, 0, 0));
// Set the initial 'from' date to 30 days before the current date
-INITIAL_FROM_DATE.setDate(INITIAL_TO_DATE.getDate() - 30);
+INITIAL_FROM_DATE_TIME.setDate(INITIAL_TO_DATE_TIME.getDate() - 30);
export default {
components: {
@@ -45,8 +45,8 @@ export default {
projectId: null,
groupId: null,
username: null,
- timeSpentFrom: INITIAL_FROM_DATE,
- timeSpentTo: INITIAL_TO_DATE,
+ timeSpentFrom: INITIAL_FROM_DATE_TIME,
+ timeSpentTo: INITIAL_TO_DATE_TIME,
cursor: {
first: ENTRIES_PER_PAGE,
after: null,
@@ -54,8 +54,8 @@ export default {
before: null,
},
queryVariables: {
- startDate: INITIAL_FROM_DATE,
- endDate: INITIAL_TO_DATE,
+ startTime: INITIAL_FROM_DATE_TIME,
+ endTime: INITIAL_TO_DATE_TIME,
projectId: null,
groupId: null,
username: null,
@@ -108,9 +108,15 @@ export default {
before: null,
};
+ const { timeSpentTo } = this;
+
+ if (timeSpentTo) {
+ timeSpentTo.setDate(timeSpentTo.getDate() + 1);
+ }
+
this.queryVariables = {
- startDate: this.nullIfBlank(this.timeSpentFrom),
- endDate: this.nullIfBlank(this.timeSpentTo),
+ startTime: this.nullIfBlank(this.timeSpentFrom),
+ endTime: this.nullIfBlank(timeSpentTo),
projectId: this.nullIfBlank(this.projectId),
groupId: this.nullIfBlank(this.groupId),
username: this.nullIfBlank(this.username),
@@ -141,8 +147,8 @@ export default {
},
i18n: {
username: s__('TimeTrackingReport|Username'),
- from: s__('TimeTrackingReport|From'),
- to: s__('TimeTrackingReport|To'),
+ from: s__('TimeTrackingReport|From the start of'),
+ to: s__('TimeTrackingReport|To the end of'),
runReport: s__('TimeTrackingReport|Run report'),
totalTimeSpentText: s__('TimeTrackingReport|Total time spent: '),
},
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 7e1e6cc445c..43aa9b94b3a 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -27,7 +27,7 @@ export default {
'CICD|Limit access %{italicStart}from%{italicEnd} this project (Deprecated)',
),
toggleHelpText: s__(
- `CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more.%{linkEnd}`,
+ `CICD|Prevent CI/CD job tokens from this project from being used to access other projects unless the other project is added to the allowlist. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more%{linkEnd}.`,
),
cardHeaderTitle: s__('CICD|Add an existing project to the scope'),
settingDisabledMessage: s__(
@@ -258,12 +258,12 @@ export default {
<template #help>
<gl-sprintf :message="$options.i18n.toggleHelpText">
<template #link="{ content }">
- <gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">
- {{ content }}
- </gl-link>
- <strong>{{ $options.i18n.disableToggleWarning }} </strong>
+ <gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">{{
+ content
+ }}</gl-link>
</template>
</gl-sprintf>
+ <strong>{{ $options.i18n.disableToggleWarning }} </strong>
</template>
</gl-toggle>
diff --git a/app/assets/javascripts/tracing/components/tracing_details.vue b/app/assets/javascripts/tracing/components/tracing_details.vue
deleted file mode 100644
index d8b2cbc9469..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_details.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import { visitUrl, isSafeURL } from '~/lib/utils/url_utility';
-
-export default {
- components: {
- GlLoadingIcon,
- },
- i18n: {
- error: s__('Tracing|Failed to load trace details.'),
- },
- props: {
- observabilityClient: {
- required: true,
- type: Object,
- },
- traceId: {
- required: true,
- type: String,
- },
- tracingIndexUrl: {
- required: true,
- type: String,
- validator: (val) => isSafeURL(val),
- },
- },
- data() {
- return {
- trace: null,
- loading: false,
- };
- },
- created() {
- this.validateAndFetch();
- },
- methods: {
- async validateAndFetch() {
- if (!this.traceId) {
- createAlert({
- message: this.$options.i18n.error,
- });
- }
- this.loading = true;
- try {
- const enabled = await this.observabilityClient.isTracingEnabled();
- if (enabled) {
- await this.fetchTrace();
- } else {
- this.goToTracingIndex();
- }
- } catch (e) {
- createAlert({
- message: this.$options.i18n.error,
- });
- } finally {
- this.loading = false;
- }
- },
- async fetchTrace() {
- this.loading = true;
- try {
- this.trace = await this.observabilityClient.fetchTrace(this.traceId);
- } catch (e) {
- createAlert({
- message: this.$options.i18n.error,
- });
- } finally {
- this.loading = false;
- }
- },
- goToTracingIndex() {
- visitUrl(this.tracingIndexUrl);
- },
- },
-};
-</script>
-
-<template>
- <div v-if="loading" class="gl-py-5">
- <gl-loading-icon size="lg" />
- </div>
-
- <!-- TODO Replace with actual trace-details component-->
- <div v-else-if="trace" data-testid="trace-details">
- <p>{{ tracingIndexUrl }}</p>
- <p>{{ trace }}</p>
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_empty_state.vue b/app/assets/javascripts/tracing/components/tracing_empty_state.vue
deleted file mode 100644
index f17060db6bc..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_empty_state.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<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,
- name: 'TracingEmptyState',
- i18n: {
- title: s__('Tracing|Get started with Tracing'),
- description: s__('Tracing|Monitor your applications with GitLab Distributed Tracing.'),
- enableButtonText: s__('Tracing|Enable'),
- },
- components: {
- GlEmptyState,
- GlButton,
- },
-};
-</script>
-
-<template>
- <gl-empty-state :title="$options.i18n.title" :svg-path="$options.EMPTY_TRACING_SVG">
- <template #description>
- <div>
- <span>{{ $options.i18n.description }}</span>
- </div>
- </template>
-
- <template #actions>
- <gl-button variant="confirm" class="gl-mx-2 gl-mb-3" @click="$emit('enable-tracing')">
- {{ $options.i18n.enableButtonText }}
- </gl-button>
- </template>
- </gl-empty-state>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_list.vue b/app/assets/javascripts/tracing/components/tracing_list.vue
deleted file mode 100644
index 21d1353a86d..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_list.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
-import UrlSync from '~/vue_shared/components/url_sync.vue';
-import {
- queryToFilterObj,
- filterObjToQuery,
- filterObjToFilterToken,
- filterTokensToFilterObj,
-} from '../filters';
-import TracingEmptyState from './tracing_empty_state.vue';
-import TracingTableList from './tracing_table_list.vue';
-import FilteredSearch from './tracing_list_filtered_search.vue';
-
-export default {
- components: {
- GlLoadingIcon,
- TracingTableList,
- TracingEmptyState,
- FilteredSearch,
- UrlSync,
- },
- props: {
- observabilityClient: {
- required: true,
- type: Object,
- },
- },
- data() {
- return {
- loading: true,
- /**
- * tracingEnabled: boolean | null.
- * null identifies a state where we don't know if tracing is enabled or not (e.g. when fetching the status from the API fails)
- */
- tracingEnabled: null,
- traces: [],
- filters: queryToFilterObj(window.location.search),
- };
- },
- computed: {
- query() {
- return filterObjToQuery(this.filters);
- },
- initialFilterValue() {
- return filterObjToFilterToken(this.filters);
- },
- },
- async created() {
- this.checkEnabled();
- },
- methods: {
- async checkEnabled() {
- this.loading = true;
- try {
- this.tracingEnabled = await this.observabilityClient.isTracingEnabled();
- if (this.tracingEnabled) {
- await this.fetchTraces();
- }
- } catch (e) {
- createAlert({
- message: s__('Tracing|Failed to load page.'),
- });
- } finally {
- this.loading = false;
- }
- },
- async enableTracing() {
- this.loading = true;
- try {
- await this.observabilityClient.enableTraces();
- this.tracingEnabled = true;
- await this.fetchTraces();
- } catch (e) {
- createAlert({
- message: s__('Tracing|Failed to enable tracing.'),
- });
- } finally {
- this.loading = false;
- }
- },
- async fetchTraces() {
- this.loading = true;
- try {
- const traces = await this.observabilityClient.fetchTraces(this.filters);
- this.traces = traces;
- } catch (e) {
- createAlert({
- message: s__('Tracing|Failed to load traces.'),
- });
- } finally {
- this.loading = false;
- }
- },
- selectTrace(trace) {
- visitUrl(joinPaths(window.location.pathname, trace.trace_id));
- },
- handleFilters(filterTokens) {
- this.filters = filterTokensToFilterObj(filterTokens);
- this.fetchTraces();
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div v-if="loading" class="gl-py-5">
- <gl-loading-icon size="lg" />
- </div>
-
- <template v-else-if="tracingEnabled !== null">
- <tracing-empty-state v-if="tracingEnabled === false" @enable-tracing="enableTracing" />
-
- <template v-else>
- <filtered-search :initial-filters="initialFilterValue" @submit="handleFilters" />
- <url-sync :query="query" />
-
- <tracing-table-list :traces="traces" @reload="fetchTraces" @trace-selected="selectTrace" />
- </template>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue b/app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue
deleted file mode 100644
index d086f2d03ff..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_list_filtered_search.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<script>
-import { GlFilteredSearch, GlFilteredSearchToken } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import {
- OPERATORS_IS,
- OPERATORS_IS_NOT,
-} from '~/vue_shared/components/filtered_search_bar/constants';
-import {
- PERIOD_FILTER_TOKEN_TYPE,
- SERVICE_NAME_FILTER_TOKEN_TYPE,
- OPERATION_FILTER_TOKEN_TYPE,
- TRACE_ID_FILTER_TOKEN_TYPE,
- DURATION_MS_FILTER_TOKEN_TYPE,
-} from '../filters';
-
-export default {
- availableTokens: [
- {
- title: s__('Tracing|Period'),
- icon: 'clock',
- type: PERIOD_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS,
- unique: true,
- options: [
- { value: '1m', title: s__('Tracing|Last 1 minute') },
- { value: '15m', title: s__('Tracing|Last 15 minutes') },
- { value: '30m', title: s__('Tracing|Last 30 minutes') },
- { value: '1h', title: s__('Tracing|Last 1 hour') },
- { value: '24h', title: s__('Tracing|Last 24 hours') },
- { value: '7d', title: s__('Tracing|Last 7 days') },
- { value: '14d', title: s__('Tracing|Last 14 days') },
- { value: '30d', title: s__('Tracing|Last 30 days') },
- ],
- },
- {
- title: s__('Tracing|Service'),
- type: SERVICE_NAME_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS_NOT,
- },
- {
- title: s__('Tracing|Operation'),
- type: OPERATION_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS_NOT,
- },
- {
- title: s__('Tracing|Trace ID'),
- type: TRACE_ID_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: OPERATORS_IS_NOT,
- },
- {
- title: s__('Tracing|Duration (ms)'),
- type: DURATION_MS_FILTER_TOKEN_TYPE,
- token: GlFilteredSearchToken,
- operators: [
- { value: '>', description: s__('Tracing|longer than') },
- { value: '<', description: s__('Tracing|shorter than') },
- ],
- },
- ],
- components: {
- GlFilteredSearch,
- },
- props: {
- initialFilters: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
-};
-</script>
-
-<template>
- <div class="vue-filtered-search-bar-container row-content-block gl-border-t-none">
- <gl-filtered-search
- :value="initialFilters"
- terms-as-tokens
- :placeholder="s__('Tracing|Filter Traces')"
- :available-tokens="$options.availableTokens"
- @submit="$emit('submit', $event)"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_table_list.vue b/app/assets/javascripts/tracing/components/tracing_table_list.vue
deleted file mode 100644
index abb1f3ae88c..00000000000
--- a/app/assets/javascripts/tracing/components/tracing_table_list.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { GlTable, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export const tableDataClass = 'gl-display-flex gl-md-display-table-cell gl-align-items-center';
-export default {
- name: 'TracingTableList',
- i18n: {
- title: s__('Tracing|Traces'),
- emptyText: s__('Tracing|No traces to display.'),
- emptyLinkText: s__('Tracing|Check again'),
- },
- fields: [
- {
- key: 'timestamp',
- label: s__('Tracing|Date'),
- tdClass: tableDataClass,
- sortable: true,
- },
- {
- key: 'service_name',
- label: s__('Tracing|Service'),
- tdClass: tableDataClass,
- sortable: true,
- },
- {
- key: 'operation',
- label: s__('Tracing|Operation'),
- tdClass: tableDataClass,
- sortable: true,
- },
- {
- key: 'duration',
- label: s__('Tracing|Duration'),
- thClass: 'gl-w-15p',
- tdClass: tableDataClass,
- sortable: true,
- },
- ],
- components: {
- GlTable,
- GlLink,
- },
- props: {
- traces: {
- required: true,
- type: Array,
- },
- },
- methods: {
- onSelect(items) {
- if (items[0]) {
- this.$emit('trace-selected', items[0]);
- }
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h4 class="gl-display-block gl-md-display-none! gl-my-5">{{ $options.i18n.title }}</h4>
-
- <gl-table
- :items="traces"
- :fields="$options.fields"
- show-empty
- sort-by="timestamp"
- :sort-desc="true"
- fixed
- stacked="md"
- tbody-tr-class="table-row"
- selectable
- select-mode="single"
- selected-variant=""
- @row-selected="onSelect"
- >
- <template #cell(timestamp)="data">
- {{ data.item.timestamp }}
- </template>
-
- <template #cell(service_name)="data">
- {{ data.item.service_name }}
- </template>
-
- <template #cell(operation)="data">
- {{ data.item.operation }}
- </template>
-
- <template #cell(duration)="data">
- <!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
- {{ `${data.item.duration} ms` }}
- </template>
-
- <template #empty>
- {{ $options.i18n.emptyText }}
- <gl-link @click="$emit('reload')">{{ $options.i18n.emptyLinkText }}</gl-link>
- </template>
- </gl-table>
- </div>
-</template>
diff --git a/app/assets/javascripts/tracing/details_index.vue b/app/assets/javascripts/tracing/details_index.vue
deleted file mode 100644
index 5702a88766c..00000000000
--- a/app/assets/javascripts/tracing/details_index.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<script>
-import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import TracingDetails from './components/tracing_details.vue';
-
-export default {
- components: {
- ObservabilityContainer,
- TracingDetails,
- },
- props: {
- traceId: {
- type: String,
- required: true,
- },
- oauthUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- tracingIndexUrl: {
- required: true,
- type: String,
- },
- },
-};
-</script>
-
-<template>
- <observability-container
- :oauth-url="oauthUrl"
- :tracing-url="tracingUrl"
- :provisioning-url="provisioningUrl"
- >
- <template #default="{ observabilityClient }">
- <tracing-details
- :trace-id="traceId"
- :tracing-index-url="tracingIndexUrl"
- :observability-client="observabilityClient"
- />
- </template>
- </observability-container>
-</template>
diff --git a/app/assets/javascripts/tracing/filters.js b/app/assets/javascripts/tracing/filters.js
deleted file mode 100644
index 88a54b2e69f..00000000000
--- a/app/assets/javascripts/tracing/filters.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import {
- filterToQueryObject,
- urlQueryToFilter,
- prepareTokens,
- processFilters,
-} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
-
-export const PERIOD_FILTER_TOKEN_TYPE = 'period';
-export const SERVICE_NAME_FILTER_TOKEN_TYPE = 'service-name';
-export const OPERATION_FILTER_TOKEN_TYPE = 'operation';
-export const TRACE_ID_FILTER_TOKEN_TYPE = 'trace-id';
-export const DURATION_MS_FILTER_TOKEN_TYPE = 'duration-ms';
-
-export function queryToFilterObj(url) {
- const filter = urlQueryToFilter(url, {
- filteredSearchTermKey: 'search',
- customOperators: [
- {
- operator: '>',
- prefix: 'gt',
- },
- {
- operator: '<',
- prefix: 'lt',
- },
- ],
- });
- const {
- period = null,
- service = null,
- operation = null,
- trace_id: traceId = null,
- durationMs = null,
- } = filter;
- const search = filter[FILTERED_SEARCH_TERM];
- return {
- period,
- service,
- operation,
- traceId,
- durationMs,
- search,
- };
-}
-
-export function filterObjToQuery(filters) {
- return filterToQueryObject(
- {
- period: filters.period,
- service: filters.serviceName,
- operation: filters.operation,
- trace_id: filters.traceId,
- durationMs: filters.durationMs,
- [FILTERED_SEARCH_TERM]: filters.search,
- },
- {
- filteredSearchTermKey: 'search',
- customOperators: [
- {
- operator: '>',
- prefix: 'gt',
- applyOnlyToKey: 'durationMs',
- },
- {
- operator: '<',
- prefix: 'lt',
- applyOnlyToKey: 'durationMs',
- },
- ],
- },
- );
-}
-
-export function filterObjToFilterToken(filters) {
- return prepareTokens({
- [PERIOD_FILTER_TOKEN_TYPE]: filters.period,
- [SERVICE_NAME_FILTER_TOKEN_TYPE]: filters.serviceName,
- [OPERATION_FILTER_TOKEN_TYPE]: filters.operation,
- [TRACE_ID_FILTER_TOKEN_TYPE]: filters.traceId,
- [DURATION_MS_FILTER_TOKEN_TYPE]: filters.durationMs,
- [FILTERED_SEARCH_TERM]: filters.search,
- });
-}
-
-export function filterTokensToFilterObj(tokens) {
- const {
- [SERVICE_NAME_FILTER_TOKEN_TYPE]: serviceName,
- [PERIOD_FILTER_TOKEN_TYPE]: period,
- [OPERATION_FILTER_TOKEN_TYPE]: operation,
- [TRACE_ID_FILTER_TOKEN_TYPE]: traceId,
- [DURATION_MS_FILTER_TOKEN_TYPE]: durationMs,
- [FILTERED_SEARCH_TERM]: search,
- } = processFilters(tokens);
-
- return {
- serviceName,
- period,
- operation,
- traceId,
- durationMs,
- search,
- };
-}
diff --git a/app/assets/javascripts/tracing/list_index.vue b/app/assets/javascripts/tracing/list_index.vue
deleted file mode 100644
index 432fbb81506..00000000000
--- a/app/assets/javascripts/tracing/list_index.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import ObservabilityContainer from '~/observability/components/observability_container.vue';
-import TracingList from './components/tracing_list.vue';
-
-export default {
- components: {
- ObservabilityContainer,
- TracingList,
- },
- props: {
- oauthUrl: {
- type: String,
- required: true,
- },
- tracingUrl: {
- type: String,
- required: true,
- },
- provisioningUrl: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <observability-container
- :oauth-url="oauthUrl"
- :tracing-url="tracingUrl"
- :provisioning-url="provisioningUrl"
- >
- <template #default="{ observabilityClient }">
- <tracing-list :observability-client="observabilityClient" />
- </template>
- </observability-container>
-</template>
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 114587bb363..88b7f6d3532 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -30,4 +30,10 @@ export const GOOGLE_ANALYTICS_ID_COOKIE_NAME = '_ga';
export const GITLAB_INTERNAL_EVENT_CATEGORY = 'InternalEventTracking';
-export const SERVICE_PING_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-0';
+export const SERVICE_PING_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1';
+
+export const SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT =
+ 'users_visiting_security_configuration_threat_management';
+
+export const SERVICE_PING_PIPELINE_SECURITY_VISIT = 'users_visiting_pipeline_security';
+export const USER_CONTEXT_SCHEMA = 'iglu:com.gitlab/user_context/jsonschema/1-0-0';
diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 89d90cf89be..99e4a6aa3c7 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -15,10 +15,14 @@ export function dispatchSnowplowEvent(
let { value } = data;
const standardContext = getStandardContext({ extra });
- const contexts = [standardContext];
+ let contexts = [standardContext];
if (data.context) {
- contexts.push(data.context);
+ if (Array.isArray(data.context)) {
+ contexts = [...contexts, ...data.context];
+ } else {
+ contexts.push(data.context);
+ }
}
if (value !== undefined) {
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index ffbd932c02b..2ee4703aa0b 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -72,4 +72,5 @@ export function initDefaultTrackers() {
InternalEvents.bindInternalEventDocument();
InternalEvents.trackInternalLoadEvents();
+ InternalEvents.initBrowserSDK();
}
diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js
index a5fbb55ff63..9bd0200cad1 100644
--- a/app/assets/javascripts/tracking/internal_events.js
+++ b/app/assets/javascripts/tracking/internal_events.js
@@ -1,10 +1,12 @@
import API from '~/api';
+import getStandardContext from './get_standard_context';
import Tracking from './tracking';
import {
GITLAB_INTERNAL_EVENT_CATEGORY,
LOAD_INTERNAL_EVENTS_SELECTOR,
SERVICE_PING_SCHEMA,
+ USER_CONTEXT_SCHEMA,
} from './constants';
import { Tracker } from './tracker';
import { InternalEventHandler, createInternalEventPayload } from './utils';
@@ -13,17 +15,24 @@ const InternalEvents = {
/**
*
* @param {string} event
+ * @param {object} data
*/
- track_event(event) {
+ track_event(event, data = {}) {
+ const { context, ...rest } = data;
+
+ const defaultContext = {
+ schema: SERVICE_PING_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: 'redis_hll',
+ },
+ };
+ const mergedContext = context ? [defaultContext, context] : defaultContext;
+
API.trackInternalEvent(event);
Tracking.event(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
- context: {
- schema: SERVICE_PING_SCHEMA,
- data: {
- event_name: event,
- data_source: 'redis_hll',
- },
- },
+ context: mergedContext,
+ ...rest,
});
},
/**
@@ -33,8 +42,8 @@ const InternalEvents = {
mixin() {
return {
methods: {
- track_event(event) {
- InternalEvents.track_event(event);
+ track_event(event, data = {}) {
+ InternalEvents.track_event(event, data);
},
},
};
@@ -78,6 +87,25 @@ const InternalEvents = {
return loadEvents;
},
+ /**
+ * Initialize browser sdk for product analytics
+ */
+ initBrowserSDK() {
+ const standardContext = getStandardContext();
+
+ if (window.glClient) {
+ window.glClient.setDocumentTitle('GitLab');
+ window.glClient.page({
+ title: 'GitLab',
+ context: [
+ {
+ schema: USER_CONTEXT_SCHEMA,
+ data: standardContext?.data || {},
+ },
+ ],
+ });
+ }
+ },
};
export default InternalEvents;
diff --git a/app/assets/javascripts/tracking/tracker.js b/app/assets/javascripts/tracking/tracker.js
index b69b1714952..b74078475b0 100644
--- a/app/assets/javascripts/tracking/tracker.js
+++ b/app/assets/javascripts/tracking/tracker.js
@@ -257,12 +257,20 @@ export const Tracker = {
const customUrl = `${pageUrl}${appendHash ? window.location.hash : ''}`;
window.snowplow('setCustomUrl', customUrl);
+ // If Browser SDK is enabled set Custom url and Referrer url
+ if (window.glClient) {
+ window.glClient?.setCustomUrl(customUrl);
+ }
if (document.referrer) {
const node = referrers.find((links) => links.originalUrl === document.referrer);
if (node) {
pageLinks.referrer = node.url;
window.snowplow('setReferrerUrl', pageLinks.referrer);
+
+ if (window.glClient) {
+ window.glClient?.setReferrerUrl(pageLinks.referrer);
+ }
}
}
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
new file mode 100644
index 00000000000..9bf6d27235c
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
@@ -0,0 +1,64 @@
+import { mockGetProjectStorageStatisticsGraphQLResponse } from 'jest/usage_quotas/storage/mock_data';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import getProjectStorageStatisticsQuery from '../queries/project_storage.query.graphql';
+import ProjectStorageApp from './project_storage_app.vue';
+
+const meta = {
+ title: 'usage_quotas/storage/project_storage_app',
+ component: ProjectStorageApp,
+};
+
+export default meta;
+
+const createTemplate = (config = {}) => {
+ let { provide, apolloProvider } = config;
+
+ if (provide == null) {
+ provide = {};
+ }
+
+ if (apolloProvider == null) {
+ const requestHandlers = [
+ [
+ getProjectStorageStatisticsQuery,
+ () => Promise.resolve(mockGetProjectStorageStatisticsGraphQLResponse),
+ ],
+ ];
+ apolloProvider = createMockApollo(requestHandlers);
+ }
+
+ return (args, { argTypes }) => ({
+ components: { ProjectStorageApp },
+ apolloProvider,
+ provide: {
+ projectPath: '/namespace/project',
+ ...provide,
+ },
+ props: Object.keys(argTypes),
+ template: '<project-storage-app />',
+ });
+};
+
+export const Default = {
+ render: createTemplate(),
+};
+
+export const Loading = {
+ render(...args) {
+ const requestHandlers = [[getProjectStorageStatisticsQuery, () => new Promise(() => {})]];
+ const apolloProvider = createMockApollo(requestHandlers);
+ return createTemplate({
+ apolloProvider,
+ })(...args);
+ },
+};
+
+export const LoadingError = {
+ render(...args) {
+ const requestHandlers = [[getProjectStorageStatisticsQuery, () => Promise.reject()]];
+ const apolloProvider = createMockApollo(requestHandlers);
+ return createTemplate({
+ apolloProvider,
+ })(...args);
+ },
+};
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
index f271b284d78..a5e1cc398e3 100644
--- a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
@@ -3,6 +3,7 @@ import { GlAlert, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { updateRepositorySize } from '~/api/projects_api';
import { numberToHumanSize } from '~/lib/utils/number_utils';
+import SectionedPercentageBar from '~/usage_quotas/components/sectioned_percentage_bar.vue';
import {
ERROR_MESSAGE,
LEARN_MORE_LABEL,
@@ -19,7 +20,6 @@ import {
} from '../constants';
import getProjectStorageStatistics from '../queries/project_storage.query.graphql';
import { getStorageTypesFromProjectStatistics, descendingStorageUsageSort } from '../utils';
-import UsageGraph from './usage_graph.vue';
import ProjectStorageDetail from './project_storage_detail.vue';
export default {
@@ -29,8 +29,8 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
- UsageGraph,
ProjectStorageDetail,
+ SectionedPercentageBar,
},
inject: ['projectPath'],
apollo: {
@@ -88,6 +88,67 @@ export default {
storageTypeHelpPaths,
);
},
+
+ sections() {
+ if (!this.project?.statistics) {
+ return null;
+ }
+
+ const {
+ buildArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ storageSize,
+ wikiSize,
+ snippetsSize,
+ } = this.project.statistics;
+
+ if (storageSize === 0) {
+ return null;
+ }
+
+ return [
+ {
+ id: 'repository',
+ value: repositorySize,
+ },
+ {
+ id: 'lfsObjects',
+ value: lfsObjectsSize,
+ },
+ {
+ id: 'packages',
+ value: packagesSize,
+ },
+ {
+ id: 'buildArtifacts',
+ value: buildArtifactsSize,
+ },
+ {
+ id: 'wiki',
+ value: wikiSize,
+ },
+ {
+ id: 'snippets',
+ value: snippetsSize,
+ },
+ ]
+ .filter((data) => data.value !== 0)
+ .sort(descendingStorageUsageSort('value'))
+ .map((storageType) => {
+ const storageTypeExtraData = PROJECT_STORAGE_TYPES.find(
+ (type) => storageType.id === type.id,
+ );
+ const label = storageTypeExtraData?.name;
+
+ return {
+ label,
+ formattedValue: numberToHumanSize(storageType.value),
+ ...storageType,
+ };
+ });
+ },
},
methods: {
clearError() {
@@ -123,11 +184,11 @@ export default {
{{ error }}
</gl-alert>
<div v-else>
- <div class="gl-pt-5 gl-px-3">
- <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div class="gl-pt-5">
+ <div class="gl-display-flex gl-justify-content-space-between">
<div>
- <h2 class="gl-m-0 gl-font-lg gl-font-weight-bold">{{ $options.TOTAL_USAGE_TITLE }}</h2>
- <p class="gl-m-0 gl-text-gray-400">
+ <h4 class="gl-font-lg gl-mb-3 gl-mt-0">{{ $options.TOTAL_USAGE_TITLE }}</h4>
+ <p>
{{ $options.TOTAL_USAGE_SUBTITLE }}
<gl-link
:href="$options.usageQuotasHelpPaths.usageQuotas"
@@ -137,13 +198,16 @@ export default {
>
</p>
</div>
- <p class="gl-m-0 gl-font-size-h-display gl-font-weight-bold" data-testid="total-usage">
+ <p
+ class="gl-m-0 gl-font-size-h-display gl-font-weight-bold gl-white-space-nowrap"
+ data-testid="total-usage"
+ >
{{ totalUsage }}
</p>
</div>
</div>
<div v-if="!isStatisticsEmpty" class="gl-w-full">
- <usage-graph :root-storage-statistics="project.statistics" :limit="0" />
+ <sectioned-percentage-bar class="gl-mt-5" :sections="sections" />
</div>
<div class="gl-w-full gl-my-5">
<gl-button
diff --git a/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue b/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue
deleted file mode 100644
index 33f202e69db..00000000000
--- a/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { PROJECT_STORAGE_TYPES } from '../constants';
-import { descendingStorageUsageSort } from '../utils';
-
-export default {
- name: 'UsageGraph',
- props: {
- rootStorageStatistics: {
- required: true,
- type: Object,
- },
- limit: {
- required: true,
- type: Number,
- },
- },
- computed: {
- storageTypes() {
- const {
- buildArtifactsSize,
- lfsObjectsSize,
- packagesSize,
- repositorySize,
- storageSize,
- wikiSize,
- snippetsSize,
- } = this.rootStorageStatistics;
-
- if (storageSize === 0) {
- return null;
- }
-
- return [
- {
- id: 'repository',
- style: this.usageStyle(this.barRatio(repositorySize)),
- class: 'gl-bg-data-viz-blue-500',
- size: repositorySize,
- },
- {
- id: 'lfsObjects',
- style: this.usageStyle(this.barRatio(lfsObjectsSize)),
- class: 'gl-bg-data-viz-orange-600',
- size: lfsObjectsSize,
- },
- {
- id: 'packages',
- style: this.usageStyle(this.barRatio(packagesSize)),
- class: 'gl-bg-data-viz-aqua-500',
- size: packagesSize,
- },
- {
- id: 'buildArtifacts',
- style: this.usageStyle(this.barRatio(buildArtifactsSize)),
- class: 'gl-bg-data-viz-green-500',
- size: buildArtifactsSize,
- },
- {
- id: 'wiki',
- style: this.usageStyle(this.barRatio(wikiSize)),
- class: 'gl-bg-data-viz-magenta-500',
- size: wikiSize,
- },
- {
- id: 'snippets',
- style: this.usageStyle(this.barRatio(snippetsSize)),
- class: 'gl-bg-data-viz-orange-800',
- size: snippetsSize,
- },
- ]
- .filter((data) => data.size !== 0)
- .sort(descendingStorageUsageSort('size'))
- .map((storageType) => {
- const storageTypeExtraData = PROJECT_STORAGE_TYPES.find(
- (type) => storageType.id === type.id,
- );
- const name = storageTypeExtraData?.name;
-
- return {
- name,
- ...storageType,
- };
- });
- },
- },
- methods: {
- formatSize(size) {
- return numberToHumanSize(size);
- },
- usageStyle(ratio) {
- return { flex: ratio };
- },
- barRatio(size) {
- let max = this.rootStorageStatistics.storageSize;
-
- if (this.limit !== 0 && max <= this.limit) {
- max = this.limit;
- }
-
- return size / max;
- },
- },
-};
-</script>
-<template>
- <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
- <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="storage-type-usage gl-h-full gl-display-inline-block"
- :class="storageType.class"
- :style="storageType.style"
- data-testid="storage-type-usage"
- ></div>
- </div>
- <div class="row gl-mb-4">
- <div
- v-for="storageType in storageTypes"
- :key="storageType.name"
- class="col-md-auto gl-display-flex gl-align-items-center"
- data-testid="storage-type-legend"
- data-qa-selector="storage_type_legend"
- >
- <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
- <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
- {{ storageType.name }}
- </span>
- <span class="gl-text-gray-500 gl-font-sm">
- {{ formatSize(storageType.size) }}
- </span>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/user_lists/components/user_list.vue b/app/assets/javascripts/user_lists/components/user_list.vue
index 29b9b68883b..d4601d1f736 100644
--- a/app/assets/javascripts/user_lists/components/user_list.vue
+++ b/app/assets/javascripts/user_lists/components/user_list.vue
@@ -39,7 +39,7 @@ export default {
),
userIdLabel: s__('UserLists|User IDs'),
userIdColumnHeader: s__('UserLists|User ID'),
- errorMessage: __('Something went wrong on our end. Please try again!'),
+ errorMessage: __('Unable to load user list. Reload the page and try again.'),
editButtonLabel: s__('UserLists|Edit'),
},
classes: {
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index ab707e7e69c..aa68cf5a161 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -32,45 +32,46 @@ function UsersSelect(currentUser, els, options = {}) {
const { handleClick, states } = options;
- $els.each((i, dropdown) => {
- const userSelect = this;
- const $dropdown = $(dropdown);
- const options = {
- states,
- projectId: $dropdown.data('projectId'),
- groupId: $dropdown.data('groupId'),
- showCurrentUser: $dropdown.data('currentUser'),
- todoFilter: $dropdown.data('todoFilter'),
- todoStateFilter: $dropdown.data('todoStateFilter'),
- iid: $dropdown.data('iid'),
- issuableType: $dropdown.data('issuableType'),
- targetBranch: $dropdown.data('targetBranch'),
- authorId: $dropdown.data('authorId'),
- showSuggested: $dropdown.data('showSuggested'),
- };
- const showNullUser = $dropdown.data('nullUser');
- const defaultNullUser = $dropdown.data('nullUserDefault');
- const showMenuAbove = $dropdown.data('showMenuAbove');
- const showAnyUser = $dropdown.data('anyUser');
- const firstUser = $dropdown.data('firstUser');
- const defaultLabel = $dropdown.data('defaultLabel');
- const issueURL = $dropdown.data('issueUpdate');
- const $selectbox = $dropdown.closest('.selectbox');
- const $assignToMeLink = $selectbox.next('.assign-to-me-link');
- let $block = $selectbox.closest('.block');
- const abilityName = $dropdown.data('abilityName');
- let $value = $block.find('.value');
- const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
- const $loading = $block.find('.block-loading').addClass('gl-display-none');
- const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
- let selectedId = $dropdown.data('selected');
- let assignTo;
- let assigneeTemplate;
- let collapsedAssigneeTemplate;
-
- const suggestedReviewersHelpPath = $dropdown.data('suggestedReviewersHelpPath');
- const suggestedReviewersHeaderTemplate = template(
- `<div class="gl-display-flex gl-align-items-center">
+ this.dropdowns = $els
+ .map((i, dropdown) => {
+ const userSelect = this;
+ const $dropdown = $(dropdown);
+ const options = {
+ states,
+ projectId: $dropdown.data('projectId'),
+ groupId: $dropdown.data('groupId'),
+ showCurrentUser: $dropdown.data('currentUser'),
+ todoFilter: $dropdown.data('todoFilter'),
+ todoStateFilter: $dropdown.data('todoStateFilter'),
+ iid: $dropdown.data('iid'),
+ issuableType: $dropdown.data('issuableType'),
+ targetBranch: $dropdown.data('targetBranch'),
+ authorId: $dropdown.data('authorId'),
+ showSuggested: $dropdown.data('showSuggested'),
+ };
+ const showNullUser = $dropdown.data('nullUser');
+ const defaultNullUser = $dropdown.data('nullUserDefault');
+ const showMenuAbove = $dropdown.data('showMenuAbove');
+ const showAnyUser = $dropdown.data('anyUser');
+ const firstUser = $dropdown.data('firstUser');
+ const defaultLabel = $dropdown.data('defaultLabel');
+ const issueURL = $dropdown.data('issueUpdate');
+ const $selectbox = $dropdown.closest('.selectbox');
+ const $assignToMeLink = $selectbox.next('.assign-to-me-link');
+ let $block = $selectbox.closest('.block');
+ const abilityName = $dropdown.data('abilityName');
+ let $value = $block.find('.value');
+ const $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ const $loading = $block.find('.block-loading').addClass('gl-display-none');
+ const selectedIdDefault = defaultNullUser && showNullUser ? 0 : null;
+ let selectedId = $dropdown.data('selected');
+ let assignTo;
+ let assigneeTemplate;
+ let collapsedAssigneeTemplate;
+
+ const suggestedReviewersHelpPath = $dropdown.data('suggestedReviewersHelpPath');
+ const suggestedReviewersHeaderTemplate = template(
+ `<div class="gl-display-flex gl-align-items-center">
<%- header %>
<a
title="${s__('SuggestedReviewers|Learn about suggested reviewers')}"
@@ -82,562 +83,568 @@ function UsersSelect(currentUser, els, options = {}) {
${spriteIcon('question-o', 'gl-ml-3 gl-icon s16')}
</a>
</div>`,
- );
+ );
- if (selectedId === undefined) {
- selectedId = selectedIdDefault;
- }
+ if (selectedId === undefined) {
+ selectedId = selectedIdDefault;
+ }
- const assignYourself = function () {
- const unassignedSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+ const assignYourself = function () {
+ const unassignedSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
- // Save current selected user to the DOM
- const currentUserInfo = $dropdown.data('currentUserInfo') || {};
- const currentUser = userSelect.currentUser || {};
- const fieldName = $dropdown.data('fieldName');
- const userName = currentUserInfo.name;
- const userId = currentUserInfo.id || currentUser.id;
+ // Save current selected user to the DOM
+ const currentUserInfo = $dropdown.data('currentUserInfo') || {};
+ const currentUser = userSelect.currentUser || {};
+ const fieldName = $dropdown.data('fieldName');
+ const userName = currentUserInfo.name;
+ const userId = currentUserInfo.id || currentUser.id;
- const inputHtmlString = template(`
+ const inputHtmlString = template(`
<input type="hidden" name="<%- fieldName %>"
data-meta="<%- userName %>"
value="<%- userId %>" />
`)({ fieldName, userName, userId });
- if ($selectbox) {
- $dropdown.parent().before(inputHtmlString);
- } else {
- $dropdown.after(inputHtmlString);
+ if ($selectbox) {
+ $dropdown.parent().before(inputHtmlString);
+ } else {
+ $dropdown.after(inputHtmlString);
+ }
+ };
+
+ if ($block[0]) {
+ $block[0].addEventListener('assignYourself', assignYourself);
}
- };
- if ($block[0]) {
- $block[0].addEventListener('assignYourself', assignYourself);
- }
+ const getSelectedUserInputs = function () {
+ return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`);
+ };
- const getSelectedUserInputs = function () {
- return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`);
- };
-
- const getSelected = function () {
- return getSelectedUserInputs()
- .map((index, input) => parseInt(input.value, 10))
- .get();
- };
-
- const checkMaxSelect = function () {
- const maxSelect = $dropdown.data('maxSelect');
- if (maxSelect) {
- const selected = getSelected();
-
- if (selected.length > maxSelect) {
- const firstSelectedId = selected[0];
- const firstSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
-
- firstSelected.remove();
-
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.removeReviewer', {
- id: firstSelectedId,
- });
- } else {
- emitSidebarEvent('sidebar.removeAssignee', {
- id: firstSelectedId,
- });
+ const getSelected = function () {
+ return getSelectedUserInputs()
+ .map((index, input) => parseInt(input.value, 10))
+ .get();
+ };
+
+ const checkMaxSelect = function () {
+ const maxSelect = $dropdown.data('maxSelect');
+ if (maxSelect) {
+ const selected = getSelected();
+
+ if (selected.length > maxSelect) {
+ const firstSelectedId = selected[0];
+ const firstSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`);
+
+ firstSelected.remove();
+
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.removeReviewer', {
+ id: firstSelectedId,
+ });
+ } else {
+ emitSidebarEvent('sidebar.removeAssignee', {
+ id: firstSelectedId,
+ });
+ }
}
}
- }
- };
-
- const getMultiSelectDropdownTitle = function (selectedUser, isSelected) {
- const selectedUsers = getSelected().filter((u) => u !== 0);
-
- const firstUser = getSelectedUserInputs()
- .map((index, input) => ({
- name: input.dataset.meta,
- value: parseInt(input.value, 10),
- }))
- .filter((u) => u.id !== 0)
- .get(0);
-
- if (selectedUsers.length === 0) {
- return s__('UsersSelect|Unassigned');
- } else if (selectedUsers.length === 1) {
- return firstUser.name;
- } else if (isSelected) {
- const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id);
+ };
+
+ const getMultiSelectDropdownTitle = function (selectedUser, isSelected) {
+ const selectedUsers = getSelected().filter((u) => u !== 0);
+
+ const firstUser = getSelectedUserInputs()
+ .map((index, input) => ({
+ name: input.dataset.meta,
+ value: parseInt(input.value, 10),
+ }))
+ .filter((u) => u.id !== 0)
+ .get(0);
+
+ if (selectedUsers.length === 0) {
+ return s__('UsersSelect|Unassigned');
+ }
+ if (selectedUsers.length === 1) {
+ return firstUser.name;
+ }
+ if (isSelected) {
+ const otherSelected = selectedUsers.filter((s) => s !== selectedUser.id);
+ return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
+ name: selectedUser.name,
+ length: otherSelected.length,
+ });
+ }
return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
- name: selectedUser.name,
- length: otherSelected.length,
+ name: firstUser.name,
+ length: selectedUsers.length - 1,
});
- }
- return sprintf(s__('UsersSelect|%{name} + %{length} more'), {
- name: firstUser.name,
- length: selectedUsers.length - 1,
- });
- };
-
- $assignToMeLink.on('click', (e) => {
- e.preventDefault();
- $(e.currentTarget).hide();
-
- if ($dropdown.data('multiSelect')) {
- assignYourself();
- checkMaxSelect();
-
- const currentUserInfo = $dropdown.data('currentUserInfo');
- $dropdown
- .find('.dropdown-toggle-text')
- .text(getMultiSelectDropdownTitle(currentUserInfo))
- .removeClass('is-default');
- } else {
- const $input = $(`input[name="${$dropdown.data('fieldName')}"]`);
- $input.val(gon.current_user_id);
- selectedId = $input.val();
- $dropdown
- .find('.dropdown-toggle-text')
- .text(gon.current_user_fullname)
- .removeClass('is-default');
- }
- });
-
- $block.on('click', '.js-assign-yourself', (e) => {
- e.preventDefault();
- return assignTo(userSelect.currentUser.id);
- });
-
- assignTo = function (selected) {
- const data = {};
- data[abilityName] = {};
- data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.removeClass('gl-display-none');
- $dropdown.trigger('loading.gl.dropdown');
-
- return axios.put(issueURL, data).then(({ data }) => {
- let user = {};
- let tooltipTitle;
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.addClass('gl-display-none');
- if (data.assignee) {
- user = {
- name: data.assignee.name,
- username: data.assignee.username,
- avatar: data.assignee.avatar_url,
- };
- tooltipTitle = escape(user.name);
+ };
+
+ $assignToMeLink.on('click', (e) => {
+ e.preventDefault();
+ $(e.currentTarget).hide();
+
+ if ($dropdown.data('multiSelect')) {
+ assignYourself();
+ checkMaxSelect();
+
+ const currentUserInfo = $dropdown.data('currentUserInfo');
+ $dropdown
+ .find('.dropdown-toggle-text')
+ .text(getMultiSelectDropdownTitle(currentUserInfo))
+ .removeClass('is-default');
} else {
- user = {
- name: s__('UsersSelect|Unassigned'),
- username: '',
- avatar: '',
- };
- tooltipTitle = s__('UsersSelect|Assignee');
+ const $input = $(`input[name="${$dropdown.data('fieldName')}"]`);
+ $input.val(gon.current_user_id);
+ selectedId = $input.val();
+ $dropdown
+ .find('.dropdown-toggle-text')
+ .text(gon.current_user_fullname)
+ .removeClass('is-default');
}
- $value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', tooltipTitle);
- fixTitle($collapsedSidebar);
+ });
- return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ $block.on('click', '.js-assign-yourself', (e) => {
+ e.preventDefault();
+ return assignTo(userSelect.currentUser.id);
});
- };
- collapsedAssigneeTemplate = template(
- `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon(
- 'user',
- )} <% } %>`,
- );
- assigneeTemplate = template(
- `<% if (username) { %> <a class="author-link gl-font-weight-bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
+
+ assignTo = function (selected) {
+ const data = {};
+ data[abilityName] = {};
+ data[abilityName].assignee_id = selected != null ? selected : null;
+ $loading.removeClass('gl-display-none');
+ $dropdown.trigger('loading.gl.dropdown');
+
+ return axios.put(issueURL, data).then(({ data }) => {
+ let user = {};
+ let tooltipTitle;
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.addClass('gl-display-none');
+ if (data.assignee) {
+ user = {
+ name: data.assignee.name,
+ username: data.assignee.username,
+ avatar: data.assignee.avatar_url,
+ };
+ tooltipTitle = escape(user.name);
+ } else {
+ user = {
+ name: s__('UsersSelect|Unassigned'),
+ username: '',
+ avatar: '',
+ };
+ tooltipTitle = s__('UsersSelect|Assignee');
+ }
+ $value.html(assigneeTemplate(user));
+ $collapsedSidebar.attr('title', tooltipTitle);
+ fixTitle($collapsedSidebar);
+
+ return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ });
+ };
+ collapsedAssigneeTemplate = template(
+ `<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> ${spriteIcon(
+ 'user',
+ )} <% } %>`,
+ );
+ assigneeTemplate = template(
+ `<% if (username) { %> <a class="author-link gl-font-weight-bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself">
${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), {
openingTag: '<a href="#" class="js-assign-yourself">',
closingTag: '</a>',
})}</span> <% } %>`,
- );
- return initDeprecatedJQueryDropdown($dropdown, {
- showMenuAbove,
- data(term, callback) {
- return userSelect.users(term, options, (users) => {
- // GitLabDropdownFilter returns this.instance
- // GitLabDropdownRemote returns this.options.instance
- const deprecatedJQueryDropdown = this.instance || this.options.instance;
- deprecatedJQueryDropdown.options.processData(term, users, callback);
- });
- },
- processData(term, dataArg, callback) {
- // Sometimes the `dataArg` can contain special dropdown items like
- // dividers which we don't want to consider here.
- const data = dataArg.filter((x) => !x.type);
-
- let users = data;
-
- // Only show assigned user list when there is no search term
- if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
- const selectedInputs = getSelectedUserInputs();
-
- // Potential duplicate entries when dealing with issue board
- // because issue board is also managed by vue
- const selectedUsers = uniqBy(selectedInputs, (a) => a.value)
- .filter((input) => {
- const userId = parseInt(input.value, 10);
- const inUsersArray = users.find((u) => u.id === userId);
-
- return !inUsersArray && userId !== 0;
- })
- .map((input) => {
- const userId = parseInt(input.value, 10);
- const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
- return {
- avatar_url: avatarUrl || avatar_url || gon.default_avatar_url,
- id: userId,
- name,
- username,
- can_merge: parseBoolean(canMerge),
- };
- });
+ );
+
+ return initDeprecatedJQueryDropdown($dropdown, {
+ showMenuAbove,
+ data(term, callback) {
+ return userSelect.users(term, options, (users) => {
+ // GitLabDropdownFilter returns this.instance
+ // GitLabDropdownRemote returns this.options.instance
+ const deprecatedJQueryDropdown = this.instance || this.options.instance;
+ deprecatedJQueryDropdown.options.processData(term, users, callback);
+ });
+ },
+ processData(term, dataArg, callback) {
+ // Sometimes the `dataArg` can contain special dropdown items like
+ // dividers which we don't want to consider here.
+ const data = dataArg.filter((x) => !x.type);
+
+ let users = data;
+
+ // Only show assigned user list when there is no search term
+ if ($dropdown.hasClass('js-multiselect') && term.length === 0) {
+ const selectedInputs = getSelectedUserInputs();
+
+ // Potential duplicate entries when dealing with issue board
+ // because issue board is also managed by vue
+ const selectedUsers = uniqBy(selectedInputs, (a) => a.value)
+ .filter((input) => {
+ const userId = parseInt(input.value, 10);
+ const inUsersArray = users.find((u) => u.id === userId);
+
+ return !inUsersArray && userId !== 0;
+ })
+ .map((input) => {
+ const userId = parseInt(input.value, 10);
+ const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
+ return {
+ avatar_url: avatarUrl || avatar_url || gon.default_avatar_url,
+ id: userId,
+ name,
+ username,
+ can_merge: parseBoolean(canMerge),
+ };
+ });
- users = data.concat(selectedUsers);
- }
+ users = data.concat(selectedUsers);
+ }
- let anyUser;
- let index;
- let len;
- let name;
- let obj;
- let showDivider;
- if (term.length === 0) {
- showDivider = 0;
- if (firstUser) {
- // Move current user to the front of the list
- for (index = 0, len = users.length; index < len; index += 1) {
- obj = users[index];
- if (obj.username === firstUser) {
- users.splice(index, 1);
- users.unshift(obj);
- break;
+ let anyUser;
+ let index;
+ let len;
+ let name;
+ let obj;
+ let showDivider;
+ if (term.length === 0) {
+ showDivider = 0;
+ if (firstUser) {
+ // Move current user to the front of the list
+ for (index = 0, len = users.length; index < len; index += 1) {
+ obj = users[index];
+ if (obj.username === firstUser) {
+ users.splice(index, 1);
+ users.unshift(obj);
+ break;
+ }
}
}
- }
- if (showNullUser) {
- showDivider += 1;
- users.unshift({
- beforeDivider: true,
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- });
- }
- if (showAnyUser) {
- showDivider += 1;
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
+ if (showNullUser) {
+ showDivider += 1;
+ users.unshift({
+ beforeDivider: true,
+ name: s__('UsersSelect|Unassigned'),
+ id: 0,
+ });
}
- anyUser = {
- beforeDivider: true,
- name,
- id: null,
- };
- users.unshift(anyUser);
- }
-
- if (showDivider) {
- users.splice(showDivider, 0, { type: 'divider' });
- }
-
- if ($dropdown.hasClass('js-multiselect')) {
- const selected = getSelected().filter((i) => i !== 0);
-
- if ($dropdown.data('showSuggested')) {
- const suggested = this.suggestedUsers(users);
- if (suggested.length) {
- users = users.filter(
- (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1),
- );
- users.splice(showDivider + 1, 0, ...suggested);
+ if (showAnyUser) {
+ showDivider += 1;
+ name = showAnyUser;
+ if (name === true) {
+ name = s__('UsersSelect|Any User');
}
+ anyUser = {
+ beforeDivider: true,
+ name,
+ id: null,
+ };
+ users.unshift(anyUser);
}
- if (selected.length > 0) {
- if ($dropdown.data('dropdownHeader')) {
- showDivider += 1;
- users.splice(showDivider, 0, {
- type: 'header',
- content: $dropdown.data('dropdownHeader'),
- });
+ if (showDivider) {
+ users.splice(showDivider, 0, { type: 'divider' });
+ }
+
+ if ($dropdown.hasClass('js-multiselect')) {
+ const selected = getSelected().filter((i) => i !== 0);
+
+ if ($dropdown.data('showSuggested')) {
+ const suggested = this.suggestedUsers(users);
+ if (suggested.length) {
+ users = users.filter(
+ (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1),
+ );
+ users.splice(showDivider + 1, 0, ...suggested);
+ }
}
- const selectedUsers = users
- .filter((u) => selected.indexOf(u.id) !== -1)
- .sort((a, b) => a.name > b.name);
+ if (selected.length > 0) {
+ if ($dropdown.data('dropdownHeader')) {
+ showDivider += 1;
+ users.splice(showDivider, 0, {
+ type: 'header',
+ content: $dropdown.data('dropdownHeader'),
+ });
+ }
- users = users.filter((u) => selected.indexOf(u.id) === -1);
+ const selectedUsers = users
+ .filter((u) => selected.indexOf(u.id) !== -1)
+ .sort((a, b) => a.name > b.name);
- selectedUsers.forEach((selectedUser) => {
- showDivider += 1;
- users.splice(showDivider, 0, selectedUser);
- });
+ users = users.filter((u) => selected.indexOf(u.id) === -1);
- users.splice(showDivider + 1, 0, { type: 'divider' });
+ selectedUsers.forEach((selectedUser) => {
+ showDivider += 1;
+ users.splice(showDivider, 0, selectedUser);
+ });
+
+ users.splice(showDivider + 1, 0, { type: 'divider' });
+ }
}
}
- }
- callback(users);
- if (showMenuAbove) {
- $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
- }
- },
- suggestedUsers(users) {
- const selected = getSelected().filter((i) => i !== 0);
- const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1);
-
- if (!suggestedUsers.length) return [];
-
- const items = [
- {
- type: 'header',
- content: suggestedReviewersHeaderTemplate({
- header: $dropdown.data('suggestedReviewersHeader'),
- }),
- },
- ...suggestedUsers,
- { type: 'header', content: $dropdown.data('allMembersHeader') },
- ];
- return items;
- },
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['name', 'username'],
- },
- selectable: true,
- fieldName: $dropdown.data('fieldName'),
- toggleLabel(selected, el, deprecatedJQueryDropdown) {
- const inputValue = deprecatedJQueryDropdown.filterInput.val();
-
- if (this.multiSelect && inputValue === '') {
- // Remove non-users from the fullData array
- const users = deprecatedJQueryDropdown.filteredFullData();
- const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown);
-
- // Update the data model
- this.processData(inputValue, users, callback);
- }
+ callback(users);
+ if (showMenuAbove) {
+ $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
+ }
+ },
+ suggestedUsers(users) {
+ const selected = getSelected().filter((i) => i !== 0);
+ const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1);
+
+ if (!suggestedUsers.length) return [];
+
+ const items = [
+ {
+ type: 'header',
+ content: suggestedReviewersHeaderTemplate({
+ header: $dropdown.data('suggestedReviewersHeader'),
+ }),
+ },
+ ...suggestedUsers,
+ { type: 'header', content: $dropdown.data('allMembersHeader') },
+ ];
+ return items;
+ },
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name', 'username'],
+ },
+ selectable: true,
+ fieldName: $dropdown.data('fieldName'),
+ toggleLabel(selected, el, deprecatedJQueryDropdown) {
+ const inputValue = deprecatedJQueryDropdown.filterInput.val();
+
+ if (this.multiSelect && inputValue === '') {
+ // Remove non-users from the fullData array
+ const users = deprecatedJQueryDropdown.filteredFullData();
+ const callback = deprecatedJQueryDropdown.parseData.bind(deprecatedJQueryDropdown);
+
+ // Update the data model
+ this.processData(inputValue, users, callback);
+ }
- if (this.multiSelect) {
- return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
- }
+ if (this.multiSelect) {
+ return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active'));
+ }
- if (selected && 'id' in selected && $(el).hasClass('is-active')) {
- $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
- if (selected.text) {
- return selected.text;
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
+ $dropdown.find('.dropdown-toggle-text').removeClass('is-default');
+ if (selected.text) {
+ return selected.text;
+ }
+ return selected.name;
}
- return selected.name;
- }
- $dropdown.find('.dropdown-toggle-text').addClass('is-default');
- return defaultLabel;
- },
- defaultLabel,
- hidden() {
- if ($dropdown.hasClass('js-multiselect')) {
- if ($dropdown.hasClass(elsClassName)) {
- if (!$dropdown.closest('.merge-request-form').length) {
- $dropdown.data('deprecatedJQueryDropdown').clearMenu();
- $dropdown.closest('.selectbox').children('input[type="hidden"]').remove();
+ $dropdown.find('.dropdown-toggle-text').addClass('is-default');
+ return defaultLabel;
+ },
+ defaultLabel,
+ hidden() {
+ if ($dropdown.hasClass('js-multiselect')) {
+ if ($dropdown.hasClass(elsClassName)) {
+ if (!$dropdown.closest('.merge-request-form').length) {
+ $dropdown.data('deprecatedJQueryDropdown').clearMenu();
+ $dropdown.closest('.selectbox').children('input[type="hidden"]').remove();
+ }
+ emitSidebarEvent('sidebar.saveReviewers');
+ } else {
+ emitSidebarEvent('sidebar.saveAssignees');
}
- emitSidebarEvent('sidebar.saveReviewers');
- } else {
- emitSidebarEvent('sidebar.saveAssignees');
}
- }
- if (!$dropdown.data('alwaysShowSelectbox')) {
- $selectbox.hide();
+ if (!$dropdown.data('alwaysShowSelectbox')) {
+ $selectbox.hide();
- // Recalculate where .value is because vue might have changed it
- $block = $selectbox.closest('.block');
- $value = $block.find('.value');
- // display:block overrides the hide-collapse rule
- $value.css('display', '');
- }
+ // Recalculate where .value is because vue might have changed it
+ $block = $selectbox.closest('.block');
+ $value = $block.find('.value');
+ // display:block overrides the hide-collapse rule
+ $value.css('display', '');
+ }
- $('.dropdown-input-field', $block).val('');
- },
- multiSelect: $dropdown.hasClass('js-multiselect'),
- inputMeta: $dropdown.data('inputMeta'),
- clicked(options) {
- const { $el, e, isMarking } = options;
- const user = options.selectedObj;
+ $('.dropdown-input-field', $block).val('');
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ inputMeta: $dropdown.data('inputMeta'),
+ clicked(options) {
+ const { $el, e, isMarking } = options;
+ const user = options.selectedObj;
- dispose($el);
+ dispose($el);
- if ($dropdown.hasClass('js-multiselect')) {
- const isActive = $el.hasClass('is-active');
- const previouslySelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`);
+ if ($dropdown.hasClass('js-multiselect')) {
+ const isActive = $el.hasClass('is-active');
+ const previouslySelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value!=0]`);
- // Enables support for limiting the number of users selected
- // Automatically removes the first on the list if more users are selected
- checkMaxSelect();
+ // Enables support for limiting the number of users selected
+ // Automatically removes the first on the list if more users are selected
+ checkMaxSelect();
- if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
- // Unassigned selected
- previouslySelected.each((index, element) => {
- element.remove();
- });
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.removeAllReviewers');
+ if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
+ // Unassigned selected
+ previouslySelected.each((index, element) => {
+ element.remove();
+ });
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.removeAllReviewers');
+ } else {
+ emitSidebarEvent('sidebar.removeAllAssignees');
+ }
+ } else if (isActive) {
+ // user selected
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.addReviewer', user);
+ } else {
+ emitSidebarEvent('sidebar.addAssignee', user);
+ }
+
+ // Remove unassigned selection (if it was previously selected)
+ const unassignedSelected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+
+ if (unassignedSelected) {
+ unassignedSelected.remove();
+ }
} else {
- emitSidebarEvent('sidebar.removeAllAssignees');
+ if (previouslySelected.length === 0) {
+ // Select unassigned because there is no more selected users
+ this.addInput($dropdown.data('fieldName'), 0, {});
+ }
+
+ // User unselected
+ if ($dropdown.hasClass(elsClassName)) {
+ emitSidebarEvent('sidebar.removeReviewer', user);
+ } else {
+ emitSidebarEvent('sidebar.removeAssignee', user);
+ }
}
- } else if (isActive) {
- // user selected
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.addReviewer', user);
+
+ if (getSelected().find((u) => u === gon.current_user_id)) {
+ $assignToMeLink.hide();
} else {
- emitSidebarEvent('sidebar.addAssignee', user);
+ $assignToMeLink.show();
}
+ }
- // Remove unassigned selection (if it was previously selected)
- const unassignedSelected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`);
+ const page = $('body').attr('data-page');
+ const isIssueIndex = page === 'projects:issues:index';
+ const isMRIndex = page === page && page === 'projects:merge_requests:index';
+ if (
+ $dropdown.hasClass('js-filter-bulk-update') ||
+ $dropdown.hasClass('js-issuable-form-dropdown')
+ ) {
+ e.preventDefault();
- if (unassignedSelected) {
- unassignedSelected.remove();
- }
- } else {
- if (previouslySelected.length === 0) {
- // Select unassigned because there is no more selected users
- this.addInput($dropdown.data('fieldName'), 0, {});
- }
+ const isSelecting = user.id !== selectedId;
+ selectedId = isSelecting ? user.id : selectedIdDefault;
- // User unselected
- if ($dropdown.hasClass(elsClassName)) {
- emitSidebarEvent('sidebar.removeReviewer', user);
+ if (selectedId === gon.current_user_id) {
+ $('.assign-to-me-link').hide();
} else {
- emitSidebarEvent('sidebar.removeAssignee', user);
+ $('.assign-to-me-link').show();
}
+ return;
}
-
- if (getSelected().find((u) => u === gon.current_user_id)) {
- $assignToMeLink.hide();
- } else {
- $assignToMeLink.show();
+ if (handleClick) {
+ e.preventDefault();
+ handleClick(user, isMarking);
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else if (!$dropdown.hasClass('js-multiselect')) {
+ const selected = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${$dropdown.data('fieldName')}']`)
+ .val();
+ return assignTo(selected);
}
- }
-
- const page = $('body').attr('data-page');
- const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = page === page && page === 'projects:merge_requests:index';
- if (
- $dropdown.hasClass('js-filter-bulk-update') ||
- $dropdown.hasClass('js-issuable-form-dropdown')
- ) {
- e.preventDefault();
- const isSelecting = user.id !== selectedId;
- selectedId = isSelecting ? user.id : selectedIdDefault;
+ // Automatically close dropdown after assignee is selected
+ // since CE has no multiple assignees
+ // EE does not have a max-select
+ if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) {
+ // Close the dropdown
+ $dropdown.dropdown('toggle');
+ }
+ },
+ id(user) {
+ return user.id;
+ },
+ opened(e) {
+ const $el = $(e.currentTarget);
+ const selected = getSelected();
+ $el.find('.is-active').removeClass('is-active');
+
+ function highlightSelected(id) {
+ $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
+ }
- if (selectedId === gon.current_user_id) {
- $('.assign-to-me-link').hide();
+ if (selected.length > 0) {
+ getSelected().forEach((selectedId) => highlightSelected(selectedId));
} else {
- $('.assign-to-me-link').show();
+ highlightSelected(selectedId);
}
- return;
- }
- if (handleClick) {
- e.preventDefault();
- handleClick(user, isMarking);
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
- return Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
- return $dropdown.closest('form').submit();
- } else if (!$dropdown.hasClass('js-multiselect')) {
- const selected = $dropdown
- .closest('.selectbox')
- .find(`input[name='${$dropdown.data('fieldName')}']`)
- .val();
- return assignTo(selected);
- }
+ },
+ updateLabel: $dropdown.data('dropdownTitle'),
+ renderRow(user) {
+ const username = user.username ? `@${user.username}` : '';
+ const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
- // Automatically close dropdown after assignee is selected
- // since CE has no multiple assignees
- // EE does not have a max-select
- if ($dropdown.data('maxSelect') && getSelected().length === $dropdown.data('maxSelect')) {
- // Close the dropdown
- $dropdown.dropdown('toggle');
- }
- },
- id(user) {
- return user.id;
- },
- opened(e) {
- const $el = $(e.currentTarget);
- const selected = getSelected();
- $el.find('.is-active').removeClass('is-active');
-
- function highlightSelected(id) {
- $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active');
- }
-
- if (selected.length > 0) {
- getSelected().forEach((selectedId) => highlightSelected(selectedId));
- } else {
- highlightSelected(selectedId);
- }
- },
- updateLabel: $dropdown.data('dropdownTitle'),
- renderRow(user) {
- const username = user.username ? `@${user.username}` : '';
- const avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
-
- let selected = false;
+ let selected = false;
- if (this.multiSelect) {
- selected = getSelected().find((u) => user.id === u);
+ if (this.multiSelect) {
+ selected = getSelected().find((u) => user.id === u);
- const { fieldName } = this;
- const field = $dropdown
- .closest('.selectbox')
- .find(`input[name='${fieldName}'][value='${user.id}']`);
+ const { fieldName } = this;
+ const field = $dropdown
+ .closest('.selectbox')
+ .find(`input[name='${fieldName}'][value='${user.id}']`);
- if (field.length) {
- selected = true;
+ if (field.length) {
+ selected = true;
+ }
+ } else {
+ selected = user.id === selectedId;
}
- } else {
- selected = user.id === selectedId;
- }
- let img = '';
- if (user.beforeDivider != null) {
- `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${escape(
- user.name,
- )}</a></li>`;
- } else {
- // 0 margin, because it's now handled by a wrapper
- img = `<img src='${avatar}' class='avatar avatar-inline gl-m-0!' width='32' />`;
- }
+ let img = '';
+ if (user.beforeDivider != null) {
+ `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${escape(
+ user.name,
+ )}</a></li>`;
+ } else {
+ // 0 margin, because it's now handled by a wrapper
+ img = `<img src='${avatar}' class='avatar avatar-inline gl-m-0!' width='32' />`;
+ }
- return userSelect.renderRow(
- options.issuableType,
- user,
- selected,
- username,
- img,
- elsClassName,
- );
- },
- });
- });
+ return userSelect.renderRow(
+ options.issuableType,
+ user,
+ selected,
+ username,
+ img,
+ elsClassName,
+ );
+ },
+ })
+ .get()
+ .map((dropdown) => dropdown.GitLabDropdownInstance);
+ })
+ .get();
}
// Return users list. Filtered by query
diff --git a/app/assets/javascripts/visibility_level/constants.js b/app/assets/javascripts/visibility_level/constants.js
index e30982985b3..0d9ededc550 100644
--- a/app/assets/javascripts/visibility_level/constants.js
+++ b/app/assets/javascripts/visibility_level/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const VISIBILITY_LEVEL_PRIVATE_STRING = 'private';
export const VISIBILITY_LEVEL_INTERNAL_STRING = 'internal';
@@ -45,6 +45,12 @@ export const PROJECT_VISIBILITY_TYPE = {
),
};
+export const ORGANIZATION_VISIBILITY_TYPE = {
+ [VISIBILITY_LEVEL_PUBLIC_STRING]: s__(
+ 'Organization|Public - The organization can be accessed without any authentication.',
+ ),
+};
+
export const VISIBILITY_TYPE_ICON = {
[VISIBILITY_LEVEL_PUBLIC_STRING]: 'earth',
[VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index 952ff9b18e9..c49c1316b1b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -4,32 +4,26 @@ import {
GlPopover,
GlSprintf,
GlLink,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
GlTooltipDirective,
} from '@gitlab/ui';
-import { sprintf, __ } from '~/locale';
export default {
+ name: 'ActionButtons',
components: {
GlButton,
GlPopover,
GlSprintf,
GlLink,
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
- widget: {
- type: String,
- required: false,
- default: '',
- },
tertiaryButtons: {
type: Array,
+ // fix `spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js` before making this required
required: false,
default: () => [],
},
@@ -41,17 +35,26 @@ export default {
};
},
computed: {
- dropdownLabel() {
- if (!this.widget) return undefined;
-
- return sprintf(__('%{widget} options'), { widget: this.widget });
- },
hasOneOption() {
return this.tertiaryButtons.length === 1;
},
hasMultipleOptions() {
return this.tertiaryButtons.length > 1;
},
+ dropdownItems() {
+ return this.tertiaryButtons.map((item) => {
+ return {
+ ...item,
+ text: item.text,
+ href: item.href,
+ extraAttrs: {
+ dataClipboardText: item.dataClipboardText,
+ dataMethod: item.dataMethod,
+ target: item.target,
+ },
+ };
+ });
+ },
},
methods: {
onClickAction(action) {
@@ -135,32 +138,18 @@ export default {
</span>
</template>
<template v-if="hasMultipleOptions">
- <gl-dropdown
+ <gl-disclosure-dropdown
v-gl-tooltip
+ :items="dropdownItems"
:title="__('Options')"
- :text="dropdownLabel"
icon="ellipsis_v"
no-caret
category="tertiary"
- right
- lazy
text-sr-only
size="small"
- toggle-class="gl-p-2!"
class="gl-display-block gl-md-display-none!"
- >
- <gl-dropdown-item
- v-for="(btn, index) in tertiaryButtons"
- :key="index"
- :href="btn.href"
- :target="btn.target"
- :data-clipboard-text="btn.dataClipboardText"
- :data-method="btn.dataMethod"
- @click="onClickAction(btn)"
- >
- {{ btn.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ @action="onClickAction"
+ />
<span v-for="(btn, index) in tertiaryButtons" :key="index">
<gl-button
:id="btn.id"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
index 5090081d281..3b62345b969 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/added_commit_message.vue
@@ -70,7 +70,8 @@ export default {
message() {
if (this.state === STATUS_CLOSED) {
return s__('mrWidgetCommitsAdded|The changes were not merged into %{targetBranch}.');
- } else if (this.isMerged) {
+ }
+ if (this.isMerged) {
return s__(
'mrWidgetCommitsAdded|Changes merged into %{targetBranch} with %{mergeCommitSha}%{squashedCommits}.',
);
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 95fa01c23f1..4ed470440cc 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
@@ -131,7 +131,8 @@ export default {
variant: 'confirm',
action: () => this.approve(),
};
- } else if (this.showUnapprove) {
+ }
+ if (this.showUnapprove) {
return {
text: s__('mrWidget|Revoke approval'),
variant: 'default',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index e79d2db4b5a..7a3dd4ca35e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,9 +1,7 @@
<script>
import { createAlert } from '~/alert';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import MRWidgetService from '../../services/mr_widget_service';
import {
@@ -25,7 +23,6 @@ export default {
DeploymentActionButton,
DeploymentViewButton,
},
- mixins: [glFeatureFlagsMixin()],
props: {
computedDeploymentStatus: {
type: String,
@@ -71,10 +68,7 @@ export default {
return this.deployment.details?.playable_build?.play_path;
},
redeployPath() {
- if (this.redeployMrWidgetFeatureFlagEnabled) {
- return this.deployment.retry_url;
- }
- return this.deployment.details?.playable_build?.retry_path;
+ return this.deployment.retry_url;
},
stopUrl() {
return this.deployment.stop_url;
@@ -82,13 +76,8 @@ export default {
environmentAvailable() {
return Boolean(this.deployment.environment_available);
},
- redeployMrWidgetFeatureFlagEnabled() {
- return this.glFeatures.reviewAppsRedeployMrWidget;
- },
showDeploymentActionButton() {
- return (
- this.redeployPath && !this.environmentAvailable && this.redeployMrWidgetFeatureFlagEnabled
- );
+ return this.redeployPath && !this.environmentAvailable;
},
},
actionsConfiguration: {
@@ -137,16 +126,6 @@ export default {
this.actionInProgress = actionName;
MRWidgetService.executeInlineAction(endpoint)
- .then((resp) => {
- if (this.redeployMrWidgetFeatureFlagEnabled) {
- return;
- }
-
- const redirectUrl = resp?.data?.redirect_url;
- if (redirectUrl) {
- visitUrl(redirectUrl);
- }
- })
.catch(() => {
createAlert({
message: errorMessage,
@@ -184,17 +163,6 @@ export default {
>
<span>{{ $options.actionsConfiguration[constants.DEPLOYING].buttonText }}</span>
</deployment-action-button>
- <deployment-action-button
- v-if="canBeManuallyRedeployed && !redeployMrWidgetFeatureFlagEnabled"
- :action-in-progress="actionInProgress"
- :actions-configuration="$options.actionsConfiguration[constants.REDEPLOYING]"
- :computed-deployment-status="computedDeploymentStatus"
- :icon="$options.btnIcons.repeat"
- container-classes="js-manual-redeploy-action"
- @click="redeploy"
- >
- <span>{{ $options.actionsConfiguration[constants.REDEPLOYING].buttonText }}</span>
- </deployment-action-button>
<deployment-view-button
v-if="hasExternalUrls && environmentAvailable"
:app-button-text="appButtonText"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
index c7d34d45f06..efe71ed569a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue
@@ -58,7 +58,8 @@ export default {
return s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
- } else if (memoryTo < memoryFrom) {
+ }
+ if (memoryTo < memoryFrom) {
return s__(
'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB',
);
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 31bf62b7e52..3e2f3ab4103 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
@@ -305,11 +305,7 @@ export default {
</script>
<template>
- <section
- class="media-section"
- data-testid="widget-extension"
- data-qa-selector="mr_widget_extension"
- >
+ <section class="media-section" data-testid="widget-extension">
<state-container
:status="statusIconName"
:is-loading="isLoadingSummary"
@@ -346,11 +342,7 @@ export default {
</template>
</template>
</div>
- <actions
- :widget="$options.label || $options.name"
- :tertiary-buttons="tertiaryActionsButtons"
- @clickedAction="onClickedAction"
- />
+ <actions :tertiary-buttons="tertiaryActionsButtons" @clickedAction="onClickedAction" />
<div
v-if="isCollapsible"
class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"
@@ -363,7 +355,6 @@ export default {
:icon="isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
category="tertiary"
data-testid="toggle-button"
- data-qa-selector="toggle_button"
size="small"
@click="toggleCollapsed"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
index fa369d23b6c..5f0fd973e84 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue
@@ -108,7 +108,6 @@ export default {
</gl-badge>
</div>
<actions
- :widget="widgetLabel"
:tertiary-buttons="data.actions"
class="gl-ml-auto gl-pl-3"
@clickedAction="onClickedAction"
@@ -128,7 +127,6 @@ export default {
:modal-id="modalId"
:level="3"
data-testid="child-content"
- data-qa-selector="child_content"
@clickedAction="onClickedAction"
/>
</li>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
index 4f8f8d6cb58..b6bcc68e5e0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -8,8 +8,10 @@ import {
function simplifyWidgetName(componentName) {
const noWidget = componentName.replace(/^Widget/, '');
+ const camelName = noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+ const tierlessName = camelName.replace(/(CE|EE)$/, '');
- return noWidget.charAt(0).toLowerCase() + noWidget.slice(1);
+ return tierlessName;
}
function baseRedisEventName(extensionName) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
index 757178ee336..83f5c1490e2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/utils.js
@@ -65,11 +65,8 @@ const createText = (text) => {
export const generateText = (text) => {
if (typeof text === 'string') {
return createText(escapeText(text));
- } else if (
- typeof text === 'object' &&
- typeof text.text === 'string' &&
- typeof text.href === 'string'
- ) {
+ }
+ if (typeof text === 'object' && typeof text.text === 'string' && typeof text.href === 'string') {
return createText(
`${
text.prependText ? `${escapeText(text.prependText)} ` : ''
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 e94e0fbe6dc..bfcd4610379 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
@@ -11,9 +11,9 @@ import {
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.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';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { MT_MERGE_STRATEGY } from '../constants';
@@ -183,7 +183,7 @@ export default {
v-gl-tooltip
:href="ciTroubleshootingDocsPath"
target="_blank"
- :title="__('About this feature')"
+ :title="__('Get more information about troubleshooting pipelines')"
class="gl-display-flex gl-align-items-center gl-ml-2"
>
<gl-icon
@@ -205,9 +205,7 @@ export default {
data-qa-selector="merge_request_pipeline_info_content"
class="gl-display-flex gl-flex-wrap gl-align-items-center gl-justify-content-space-between"
>
- <p
- class="mr-pipeline-title gl-m-0! gl-mr-3! gl-font-weight-bold gl-line-height-32 gl-text-gray-900"
- >
+ <p class="mr-pipeline-title gl-m-0! gl-mr-3! gl-font-weight-bold gl-text-gray-900">
{{ pipeline.details.event_type_name }}
<gl-link
:href="pipeline.path"
@@ -253,7 +251,7 @@ export default {
v-safe-html="sourceBranchLink"
:title="sourceBranch"
truncate-target="child"
- class="label-branch label-truncate gl-font-weight-normal gl-vertical-align-text-bottom"
+ class="label-branch label-truncate gl-font-weight-normal"
/>
</template>
<template v-if="finishedAt">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index 400759aa086..4f39bd1d972 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -38,7 +38,7 @@ export default {
},
modifyLinkMessage() {
if (this.isFastForwardEnabled) return __('Modify commit message');
- else if (this.isSquashEnabled) return __('Modify commit messages');
+ if (this.isSquashEnabled) return __('Modify commit messages');
return __('Modify merge commit');
},
ariaLabel() {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index 61eec503951..bf2c5e52184 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -30,9 +30,11 @@ export default {
failedText() {
if (this.mr.approvals && !this.mr.isApproved) {
return this.$options.i18n.approvalNeeded;
- } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
+ }
+ if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
return this.$options.i18n.blockingMergeRequests;
- } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS) {
+ }
+ if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.EXTERNAL_STATUS_CHECKS) {
return this.$options.i18n.externalStatusChecksFailed;
}
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 7071759b8bb..0ce8389579d 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
@@ -488,7 +488,7 @@ export default {
mergeAndSquashCommitTemplatesHintText: s__(
'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more%{linkEnd}.',
),
- sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch'),
+ sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch.'),
divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
};
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 9bb39ba22e0..8249dffcc27 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
@@ -5,6 +5,7 @@ export default {
import(
'~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue'
),
+ MrTestReportWidget: () => import('~/vue_merge_request_widget/extensions/test_report/index.vue'),
MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'),
MrCodeQualityWidget: () =>
import('~/vue_merge_request_widget/extensions/code_quality/index.vue'),
@@ -18,6 +19,10 @@ export default {
},
computed: {
+ testReportWidget() {
+ return this.mr.testResultsPath && 'MrTestReportWidget';
+ },
+
terraformPlansWidget() {
return this.mr.terraformReportsPath && 'MrTerraformWidget';
},
@@ -27,9 +32,12 @@ export default {
},
widgets() {
- return [this.codeQualityWidget, this.terraformPlansWidget, 'MrSecurityWidget'].filter(
- (w) => w,
- );
+ return [
+ this.codeQualityWidget,
+ this.testReportWidget,
+ this.terraformPlansWidget,
+ 'MrSecurityWidget',
+ ].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 618d1e71f81..72c041759d9 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
@@ -92,7 +92,6 @@ export default {
</div>
<actions
v-if="hasActionButtons"
- :widget="widgetName"
:tertiary-buttons="data.actions"
class="gl-ml-auto gl-pl-3"
@clickedAction="onClickedAction"
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 2c8bf90064e..d17be3e4037 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
@@ -368,7 +368,6 @@ export default {
<slot name="action-buttons">
<action-buttons
v-if="actionButtons.length > 0"
- :widget="widgetName"
:tertiary-buttons="actionButtons"
@clickedAction="onActionClick"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
index e67924d28ab..bb82da7796a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
@@ -128,11 +128,7 @@ export default {
>
</template>
</help-popover>
- <action-buttons
- v-if="hasActionButtons"
- :widget="widgetName"
- :tertiary-buttons="actionButtons"
- />
+ <action-buttons v-if="hasActionButtons" :tertiary-buttons="actionButtons" />
</div>
</div>
<div class="gl-display-flex gl-align-items-baseline">
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index 713c9e610b3..3af984dcf6c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -23,14 +23,17 @@ export default {
const { newErrors, resolvedErrors, parsingInProgress } = data;
if (parsingInProgress) {
return i18n.loading;
- } else if (newErrors.length >= 1 && resolvedErrors.length >= 1) {
+ }
+ if (newErrors.length >= 1 && resolvedErrors.length >= 1) {
return i18n.improvementAndDegradationCopy(
i18n.findings(resolvedErrors, codeQualityPrefixes.fixed),
i18n.findings(newErrors, codeQualityPrefixes.new),
);
- } else if (resolvedErrors.length >= 1) {
+ }
+ if (resolvedErrors.length >= 1) {
return i18n.singularCopy(i18n.findings(resolvedErrors, codeQualityPrefixes.fixed));
- } else if (newErrors.length >= 1) {
+ }
+ if (newErrors.length >= 1) {
return i18n.singularCopy(i18n.findings(newErrors, codeQualityPrefixes.new));
}
return i18n.noChanges;
@@ -38,7 +41,8 @@ export default {
statusIcon() {
if (this.collapsedData.newErrors.length >= 1) {
return EXTENSION_ICONS.warning;
- } else if (this.collapsedData.resolvedErrors.length >= 1) {
+ }
+ if (this.collapsedData.resolvedErrors.length >= 1) {
return EXTENSION_ICONS.success;
}
return EXTENSION_ICONS.neutral;
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 d30acf24684..cd3a98effa3 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
@@ -36,9 +36,11 @@ export default {
if (!this.pollingFinished) {
return { title: i18n.loading };
- } else if (this.hasError) {
+ }
+ if (this.hasError) {
return { title: i18n.error };
- } else if (
+ }
+ if (
this.collapsedData?.new_errors?.length >= 1 &&
this.collapsedData?.resolved_errors?.length >= 1
) {
@@ -48,11 +50,13 @@ export default {
i18n.findings(new_errors, codeQualityPrefixes.new),
),
};
- } else if (this.collapsedData?.resolved_errors?.length >= 1) {
+ }
+ if (this.collapsedData?.resolved_errors?.length >= 1) {
return {
title: i18n.singularCopy(i18n.findings(resolved_errors, codeQualityPrefixes.fixed)),
};
- } else if (this.collapsedData?.new_errors?.length >= 1) {
+ }
+ if (this.collapsedData?.new_errors?.length >= 1) {
return { title: i18n.singularCopy(i18n.findings(new_errors, codeQualityPrefixes.new)) };
}
return { title: i18n.noChanges };
@@ -95,7 +99,8 @@ export default {
statusIcon() {
if (this.collapsedData?.new_errors?.length >= 1) {
return EXTENSION_ICONS.warning;
- } else if (this.collapsedData?.resolved_errors?.length >= 1) {
+ }
+ if (this.collapsedData?.resolved_errors?.length >= 1) {
return EXTENSION_ICONS.success;
}
return EXTENSION_ICONS.neutral;
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
deleted file mode 100644
index 6ac462d4ad5..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.js
+++ /dev/null
@@ -1,189 +0,0 @@
-import { uniqueId } from 'lodash';
-import { __ } from '~/locale';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
-import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
-import { EXTENSION_ICONS } from '../../constants';
-import {
- summaryTextBuilder,
- reportTextBuilder,
- reportSubTextBuilder,
- countRecentlyFailedTests,
- recentFailuresTextBuilder,
- formatFilePath,
-} from './utils';
-import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
-
-export default {
- name: 'WidgetTestSummary',
- enablePolling: true,
- i18n,
- props: ['testResultsPath', 'headBlobPath', 'pipeline'],
- modalComponent: TestCaseDetails,
- computed: {
- failedTestNames() {
- if (!this.collapsedData?.suites) {
- return '';
- }
-
- const newFailures = this.collapsedData?.suites.flatMap((suite) => [suite.new_failures || []]);
- const fileNames = newFailures.flatMap((newFailure) => {
- return newFailure.map((failure) => {
- return failure.file;
- });
- });
-
- return fileNames.join(' ').trim();
- },
- summary(data) {
- if (data.parsingInProgress) {
- return this.$options.i18n.loading;
- }
- if (data.hasSuiteError) {
- return this.$options.i18n.error;
- }
- return {
- subject: summaryTextBuilder(this.$options.i18n.label, data.summary),
- meta: recentFailuresTextBuilder(data.summary),
- };
- },
- statusIcon(data) {
- if (data.status === TESTS_FAILED_STATUS) {
- return EXTENSION_ICONS.warning;
- }
- if (data.hasSuiteError) {
- return EXTENSION_ICONS.failed;
- }
- return EXTENSION_ICONS.success;
- },
- tertiaryButtons() {
- const actionButtons = [];
-
- if (this.failedTestNames().length > 0) {
- actionButtons.push({
- dataClipboardText: this.failedTestNames(),
- id: uniqueId('copy-to-clipboard'),
- icon: 'copy-to-clipboard',
- testId: 'copy-failed-specs-btn',
- text: this.$options.i18n.copyFailedSpecs,
- tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
- tooltipOnClick: __('Copied'),
- });
- }
-
- actionButtons.push({
- text: this.$options.i18n.fullReport,
- href: `${this.pipeline.path}/test_report`,
- target: '_blank',
- trackFullReportClicked: true,
- testId: 'full-report-link',
- });
-
- return actionButtons;
- },
- },
- methods: {
- fetchCollapsedData() {
- return axios.get(this.testResultsPath).then((response) => {
- const { data = {}, status } = response;
- const { suites = [], summary = {} } = data;
-
- return {
- ...response,
- data: {
- hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
- parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
- ...data,
- summary: {
- recentlyFailed: countRecentlyFailedTests(suites),
- ...summary,
- },
- },
- };
- });
- },
- fetchFullData() {
- return Promise.resolve(this.prepareReports());
- },
- suiteIcon(suite) {
- if (suite.status === ERROR_STATUS) {
- return EXTENSION_ICONS.error;
- }
- if (suite.status === TESTS_FAILED_STATUS) {
- return EXTENSION_ICONS.failed;
- }
- return EXTENSION_ICONS.success;
- },
- testHeader(test, sectionHeader, index) {
- const headers = [];
- if (index === 0) {
- headers.push(sectionHeader);
- }
- if (test.recent_failures?.count && test.recent_failures?.base_branch) {
- headers.push(i18n.recentFailureCount(test.recent_failures));
- }
- return headers;
- },
- mapTestAsChild({ iconName, sectionHeader }) {
- return (test, index) => {
- return {
- id: uniqueId('test-'),
- header: this.testHeader(test, sectionHeader, index),
- modal: {
- text: test.name,
- onClick: () => {
- this.modalData = {
- testCase: {
- filePath: test.file && `${this.headBlobPath}/${formatFilePath(test.file)}`,
- ...test,
- },
- };
- },
- },
- icon: { name: iconName },
- };
- };
- },
- prepareReports() {
- return this.collapsedData.suites
- .map((suite) => {
- return {
- ...suite,
- summary: {
- recentlyFailed: countRecentlyFailedTests(suite),
- ...suite.summary,
- },
- };
- })
- .map((suite) => {
- return {
- id: uniqueId('suite-'),
- text: reportTextBuilder(suite),
- subtext: reportSubTextBuilder(suite),
- icon: {
- name: this.suiteIcon(suite),
- },
- children: [
- ...[...suite.new_failures, ...suite.new_errors].map(
- this.mapTestAsChild({
- sectionHeader: i18n.newHeader,
- iconName: EXTENSION_ICONS.failed,
- }),
- ),
- ...[...suite.existing_failures, ...suite.existing_errors].map(
- this.mapTestAsChild({
- iconName: EXTENSION_ICONS.failed,
- }),
- ),
- ...[...suite.resolved_failures, ...suite.resolved_errors].map(
- this.mapTestAsChild({
- sectionHeader: i18n.fixedHeader,
- iconName: EXTENSION_ICONS.success,
- }),
- ),
- ],
- };
- });
- },
- },
-};
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
new file mode 100644
index 00000000000..1b03b9c04e1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/index.vue
@@ -0,0 +1,313 @@
+<script>
+import { uniqueId, uniq } from 'lodash';
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import TestCaseDetails from '~/ci/pipeline_details/test_reports/test_case_details.vue';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import MrWidgetRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import { EXTENSION_ICONS } from '../../constants';
+import {
+ summaryTextBuilder,
+ reportTextBuilder,
+ reportSubTextBuilder,
+ countRecentlyFailedTests,
+ recentFailuresTextBuilder,
+ formatFilePath,
+} from './utils';
+import { i18n, TESTS_FAILED_STATUS, ERROR_STATUS } from './constants';
+
+export default {
+ name: 'WidgetTestReport',
+ components: {
+ MrWidget,
+ MrWidgetRow,
+ DynamicScroller,
+ DynamicScrollerItem,
+ TestCaseDetails,
+ },
+ i18n,
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ collapsedData: {},
+ suites: [],
+ modalData: null,
+ };
+ },
+ computed: {
+ failedTestNames() {
+ const { data: { suites = [] } = {} } = this.collapsedData;
+
+ if (!this.hasSuites) {
+ return '';
+ }
+
+ const newFailures = suites.flatMap((suite) => [suite.new_failures || []]);
+ const fileNames = newFailures.flatMap((newFailure) => {
+ return newFailure.map((failure) => {
+ return failure.file;
+ });
+ });
+
+ return uniq(fileNames).join(' ').trim();
+ },
+ summary() {
+ const {
+ data: { parsingInProgress = false, hasSuiteError = false, summary = {} } = {},
+ } = this.collapsedData;
+
+ if (parsingInProgress) {
+ return { title: this.$options.i18n.loading };
+ }
+ if (hasSuiteError) {
+ return { title: this.$options.i18n.error };
+ }
+ return {
+ title: summaryTextBuilder(this.$options.i18n.label, summary),
+ subtitle: recentFailuresTextBuilder(summary),
+ };
+ },
+ statusIcon() {
+ const { data: { status = null, hasSuiteError = false } = {} } = this.collapsedData;
+
+ if (status === TESTS_FAILED_STATUS) {
+ return EXTENSION_ICONS.warning;
+ }
+ if (hasSuiteError) {
+ return EXTENSION_ICONS.failed;
+ }
+ return EXTENSION_ICONS.success;
+ },
+ tertiaryButtons() {
+ const actionButtons = [];
+
+ if (this.failedTestNames.length > 0) {
+ actionButtons.push({
+ dataClipboardText: this.failedTestNames,
+ id: uniqueId('copy-to-clipboard'),
+ icon: 'copy-to-clipboard',
+ testId: 'copy-failed-specs-btn',
+ text: this.$options.i18n.copyFailedSpecs,
+ tooltipText: this.$options.i18n.copyFailedSpecsTooltip,
+ tooltipOnClick: __('Copied'),
+ });
+ }
+
+ actionButtons.push({
+ text: this.$options.i18n.fullReport,
+ href: `${this.mr.pipeline.path}/test_report`,
+ target: '_blank',
+ trackFullReportClicked: true,
+ testId: 'full-report-link',
+ });
+
+ return actionButtons;
+ },
+ testResultsPath() {
+ return this.mr.testResultsPath;
+ },
+ hasSuites() {
+ return this.suites.length > 0;
+ },
+ },
+ methods: {
+ fetchCollapsedData() {
+ return axios.get(this.testResultsPath).then((response) => {
+ const { data = {}, status } = response;
+ const { suites = [], summary = {} } = data;
+
+ this.collapsedData = {
+ ...response,
+ data: {
+ hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
+ parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
+ ...data,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(suites),
+ ...summary,
+ },
+ },
+ };
+ this.suites = this.prepareSuites(this.collapsedData);
+
+ return response;
+ });
+ },
+ suiteIcon(suite) {
+ if (suite.status === ERROR_STATUS) {
+ return EXTENSION_ICONS.error;
+ }
+ if (suite.status === TESTS_FAILED_STATUS) {
+ return EXTENSION_ICONS.failed;
+ }
+ return EXTENSION_ICONS.success;
+ },
+ testHeader(test, sectionHeader, index) {
+ const headers = [];
+ if (index === 0) {
+ headers.push(sectionHeader);
+ }
+ if (test.recent_failures?.count && test.recent_failures?.base_branch) {
+ headers.push(i18n.recentFailureCount(test.recent_failures));
+ }
+ return headers;
+ },
+ mapTestAsChild({ iconName, sectionHeader }) {
+ return (test, index) => {
+ return {
+ id: uniqueId('test-'),
+ header: this.testHeader(test, sectionHeader, index),
+ text: test.name,
+ actions: [
+ {
+ text: __('View details'),
+ onClick: () => {
+ this.modalData = {
+ testCase: {
+ filePath: test.file && `${this.mr.headBlobPath}/${formatFilePath(test.file)}`,
+ ...test,
+ },
+ };
+ },
+ },
+ ],
+ icon: { name: iconName },
+ };
+ };
+ },
+ onModalHidden() {
+ this.modalData = null;
+ },
+ prepareSuites(collapsedData) {
+ const {
+ data: { suites = [] },
+ } = collapsedData;
+
+ return suites
+ .map((suite) => {
+ return {
+ ...suite,
+ summary: {
+ recentlyFailed: countRecentlyFailedTests(suite),
+ ...suite.summary,
+ },
+ };
+ })
+ .map((suite) => {
+ return {
+ id: uniqueId('suite-'),
+ text: reportTextBuilder(suite),
+ subtext: reportSubTextBuilder(suite),
+ icon: {
+ name: this.suiteIcon(suite),
+ },
+ children: [
+ ...[...suite.new_failures, ...suite.new_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.newHeader,
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.existing_failures, ...suite.existing_errors].map(
+ this.mapTestAsChild({
+ iconName: EXTENSION_ICONS.failed,
+ }),
+ ),
+ ...[...suite.resolved_failures, ...suite.resolved_errors].map(
+ this.mapTestAsChild({
+ sectionHeader: i18n.fixedHeader,
+ iconName: EXTENSION_ICONS.success,
+ }),
+ ),
+ ],
+ };
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <mr-widget
+ :error-text="$options.i18n.error"
+ :status-icon-name="statusIcon"
+ :loading-text="$options.i18n.loading"
+ :action-buttons="tertiaryButtons"
+ :help-popover="$options.helpPopover"
+ :widget-name="$options.name"
+ :summary="summary"
+ :fetch-collapsed-data="fetchCollapsedData"
+ :is-collapsible="hasSuites"
+ >
+ <template #content>
+ <mr-widget-row
+ v-for="suite in suites"
+ :key="suite.id"
+ :level="2"
+ :status-icon-name="suite.icon.name"
+ :widget-name="$options.name"
+ data-testid="extension-list-item"
+ >
+ <template #header>
+ <div class="gl-flex-direction-column">
+ <div>{{ suite.text }}</div>
+ <div
+ v-for="(subtext, i) in suite.subtext"
+ :key="`${suite.id}-subtext-${i}`"
+ class="gl-font-sm gl-text-gray-700"
+ >
+ {{ subtext }}
+ </div>
+ </div>
+ </template>
+ <template #body>
+ <div v-if="suite.children.length > 0" class="gl-mt-2 gl-w-full">
+ <dynamic-scroller
+ :items="suite.children"
+ :min-item-size="32"
+ :style="{ maxHeight: '170px' }"
+ key-field="id"
+ class="gl-pr-5"
+ >
+ <template #default="{ item, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <strong
+ v-for="(headerText, i) in item.header"
+ :key="`${item.id}-headerText-${i}`"
+ class="gl-display-block gl-mt-2"
+ >
+ {{ headerText }}
+ </strong>
+ <mr-widget-row
+ :key="item.id"
+ :level="3"
+ :widget-name="$options.name"
+ :status-icon-name="item.icon.name"
+ :action-buttons="item.actions"
+ class="gl-mt-2"
+ >
+ <template #header>{{ item.text }}</template>
+ </mr-widget-row>
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
+ </div>
+ </template>
+ </mr-widget-row>
+ </template>
+ </mr-widget>
+ <test-case-details
+ :modal-id="`modal${$options.name}`"
+ :visible="modalData !== null"
+ v-bind="modalData"
+ @hidden="onModalHidden"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
index 37f9964d23a..24f6b3e69ff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/test_report/utils.js
@@ -62,7 +62,7 @@ export const reportSubTextBuilder = ({ suite_errors: suiteErrors, summary }) =>
}
return errors;
}
- return recentFailuresTextBuilder(summary);
+ return [recentFailuresTextBuilder(summary)];
};
export const countRecentlyFailedTests = (subject) => {
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 acdcbf7afd7..175a0b0563f 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
@@ -56,7 +56,6 @@ import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variab
import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
import accessibilityExtension from './extensions/accessibility';
-import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
@@ -225,9 +224,6 @@ export default {
this.mr.mergePipelinesEnabled && this.mr.sourceProjectId !== this.mr.targetProjectId,
);
},
- shouldRenderTestReport() {
- return Boolean(this.mr?.testResultsPath);
- },
mergeError() {
let { mergeError } = this.mr;
@@ -281,11 +277,6 @@ export default {
this.registerAccessibilityExtension();
}
},
- shouldRenderTestReport(newVal) {
- if (newVal) {
- this.registerTestReportExtension();
- }
- },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -525,11 +516,6 @@ export default {
registerExtension(accessibilityExtension);
}
},
- registerTestReportExtension() {
- if (this.shouldRenderTestReport) {
- registerExtension(testReportExtension);
- }
- },
},
};
</script>
@@ -569,7 +555,7 @@ export default {
v-if="hasMergeError"
type="danger"
dismissible
- data-testid="merge_error"
+ data-testid="merge-error"
>
<span v-safe-html="mergeError"></span>
</mr-widget-alert-message>
@@ -577,6 +563,7 @@ export default {
v-if="showMergePipelineForkWarning"
type="warning"
:help-path="mr.mergeRequestPipelinesHelpPath"
+ data-testid="merge-pipeline-fork-warning"
>
{{
s__(
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index f90056a8e1a..d6bab074f3f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -4,31 +4,44 @@ import { stateKey } from './state_maps';
export default function deviseState() {
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.PREPARING) {
return stateKey.preparing;
- } else if (!this.commitsCount) {
+ }
+ if (!this.commitsCount) {
return stateKey.nothingToMerge;
- } else if (this.projectArchived) {
+ }
+ if (this.projectArchived) {
return stateKey.archived;
- } else if (this.branchMissing) {
+ }
+ if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
return stateKey.checking;
- } else if (this.hasConflicts) {
+ }
+ if (this.hasConflicts) {
return stateKey.conflicts;
- } else if (this.shouldBeRebased) {
+ }
+ if (this.shouldBeRebased) {
return stateKey.rebase;
- } else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
+ }
+ if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
return stateKey.pipelineFailed;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
return stateKey.draft;
- } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
+ }
+ if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
return stateKey.unresolvedDiscussions;
- } else if (this.canMerge && this.isSHAMismatch) {
+ }
+ if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
- } else if (this.autoMergeEnabled && !this.mergeError) {
+ }
+ if (this.autoMergeEnabled && !this.mergeError) {
return stateKey.autoMergeEnabled;
- } else if (
+ }
+ if (
this.detailedMergeStatus === DETAILED_MERGE_STATUS.MERGEABLE ||
this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_STILL_RUNNING
) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index b1c069d9b1e..bb74f82145f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,8 +1,8 @@
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
-import { badgeState } from '~/issuable/components/status_box.vue';
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN } from '~/issues/constants';
import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
+import { badgeState } from '~/merge_requests/components/merge_request_status_badge.vue';
import {
MTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
@@ -351,11 +351,14 @@ export default class MergeRequestStore {
if (availableAutoMergeStrategies.includes(MTWPS_MERGE_STRATEGY)) {
return MTWPS_MERGE_STRATEGY;
- } else if (availableAutoMergeStrategies.includes(MT_MERGE_STRATEGY)) {
+ }
+ if (availableAutoMergeStrategies.includes(MT_MERGE_STRATEGY)) {
return MT_MERGE_STRATEGY;
- } else if (availableAutoMergeStrategies.includes(MWCP_MERGE_STRATEGY)) {
+ }
+ if (availableAutoMergeStrategies.includes(MWCP_MERGE_STRATEGY)) {
return MWCP_MERGE_STRATEGY;
- } else if (availableAutoMergeStrategies.includes(MWPS_MERGE_STRATEGY)) {
+ }
+ if (availableAutoMergeStrategies.includes(MWPS_MERGE_STRATEGY)) {
return MWPS_MERGE_STRATEGY;
}
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
index 106dd7a3b97..957e642fcb8 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js
@@ -1 +1,5 @@
export const HIGHLIGHT_CLASS_NAME = 'hll';
+export const MARKUP_FILE_TYPE = 'markup';
+export const MARKUP_CONTENT_SELECTOR = '.js-markup-content';
+export const ELEMENTS_PER_CHUNK = 20;
+export const CONTENT_LOADED_EVENT = 'richContentLoaded';
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
index 11ce6afbb1d..27bdcc69120 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue
@@ -3,7 +3,14 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import { sanitize } from '~/lib/dompurify';
import ViewerMixin from './mixins';
+import {
+ MARKUP_FILE_TYPE,
+ MARKUP_CONTENT_SELECTOR,
+ ELEMENTS_PER_CHUNK,
+ CONTENT_LOADED_EVENT,
+} from './constants';
export default {
components: {
@@ -16,21 +23,77 @@ export default {
data() {
return {
isLoading: true,
+ initialContent: null,
+ remainingContent: [],
};
},
+ computed: {
+ rawContent() {
+ return this.initialContent || this.richViewer || this.content;
+ },
+ isMarkup() {
+ return this.type === MARKUP_FILE_TYPE;
+ },
+ },
+ created() {
+ this.optimizeMarkupRendering();
+ },
mounted() {
- window.requestIdleCallback(async () => {
+ this.renderRemainingMarkup();
+ handleBlobRichViewer(this.$refs.content, this.type);
+ handleLocationHash();
+ },
+ methods: {
+ optimizeMarkupRendering() {
+ /**
+ * If content is markup we optimize rendering by splitting it into two parts:
+ * - initialContent (top section of the file - is rendered right away)
+ * - remainingContent (remaining content - is rendered over a longer time period)
+ *
+ * This is done so that the browser doesn't render the whole file at once (improves TBT)
+ */
+
+ if (!this.isMarkup) return;
+
+ const tmpWrapper = document.createElement('div');
+ tmpWrapper.innerHTML = sanitize(this.rawContent, this.$options.safeHtmlConfig);
+
+ const fileContent = tmpWrapper.querySelector(MARKUP_CONTENT_SELECTOR);
+ if (!fileContent) return;
+
+ const initialContent = [...fileContent.childNodes].slice(0, ELEMENTS_PER_CHUNK);
+ this.remainingContent = [...fileContent.childNodes].slice(ELEMENTS_PER_CHUNK);
+
+ fileContent.innerHTML = '';
+ fileContent.append(...initialContent);
+ this.initialContent = tmpWrapper.outerHTML;
+ },
+ renderRemainingMarkup() {
/**
- * Rendering Markdown usually takes long due to the amount of HTML being parsed.
- * This ensures that content is loaded only when the browser goes into idle.
+ * Rendering large Markdown files can block the main thread due to the amount of HTML being parsed.
+ * The optimization below ensures that content is rendered over a longer time period instead of all at once.
* More details here: https://gitlab.com/gitlab-org/gitlab/-/issues/331448
* */
- this.isLoading = false;
- await this.$nextTick();
- handleBlobRichViewer(this.$refs.content, this.type);
- handleLocationHash();
- this.$emit('richContentLoaded');
- });
+
+ if (!this.isMarkup || !this.remainingContent.length) {
+ this.$emit(CONTENT_LOADED_EVENT);
+ this.isLoading = false;
+ return;
+ }
+
+ const fileContent = this.$refs.content.$el.querySelector(MARKUP_CONTENT_SELECTOR);
+
+ for (let i = 0; i < this.remainingContent.length; i += ELEMENTS_PER_CHUNK) {
+ const nextChunkEnd = i + ELEMENTS_PER_CHUNK;
+ const content = this.remainingContent.slice(i, nextChunkEnd);
+ setTimeout(() => {
+ fileContent.append(...content);
+ if (nextChunkEnd < this.remainingContent.length) return;
+ this.$emit(CONTENT_LOADED_EVENT);
+ this.isLoading = false;
+ }, i);
+ }
+ },
},
safeHtmlConfig: {
ADD_TAGS: ['gl-emoji', 'copy-code'],
@@ -39,8 +102,8 @@ export default {
</script>
<template>
<markdown-field-view
- v-if="!isLoading"
ref="content"
- v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
+ v-safe-html:[$options.safeHtmlConfig]="rawContent"
+ :is-loading="isLoading"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 14e99977a85..2a47e96b2e2 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -50,11 +50,14 @@ export default {
tooltipTitle() {
if (!this.showTooltip) {
return undefined;
- } else if (this.file.deleted) {
+ }
+ if (this.file.deleted) {
return __('Deleted');
- } else if (this.file.tempFile) {
+ }
+ if (this.file.tempFile) {
return __('Added');
- } else if (this.file.changed) {
+ }
+ if (this.file.changed) {
return __('Modified');
}
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index 9aa7a7d6c49..1f45b4c5c9d 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,7 @@
<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.
@@ -48,7 +49,7 @@ export default {
required: false,
default: true,
},
- badgeSize: {
+ size: {
type: String,
required: false,
default: badgeSizeOptions.md,
@@ -59,7 +60,7 @@ export default {
},
computed: {
isSmallBadgeSize() {
- return this.badgeSize === badgeSizeOptions.sm;
+ return this.size === badgeSizeOptions.sm;
},
title() {
return !this.showText ? this.status?.text : '';
@@ -120,13 +121,12 @@ export default {
<template>
<gl-badge
v-gl-tooltip
- :class="{ 'gl-pl-0!': isSmallBadgeSize }"
+ :class="{ 'gl-pl-2': isSmallBadgeSize }"
:title="title"
:href="detailsPath"
- :size="badgeSize"
+ :size="size"
:variant="badgeStyles.variant"
- :data-testid="`ci-badge-${status.text}`"
- data-qa-selector="status_badge_link"
+ data-testid="ci-badge-link"
@click="$emit('ciStatusBadgeClick')"
>
<ci-icon :status="status" />
diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
index 31c98d1e3a7..025e38a55ad 100644
--- a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
@@ -1,10 +1,11 @@
<script>
-import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { confidentialityInfoText } from '../constants';
export default {
components: {
GlBadge,
+ GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -18,22 +19,31 @@ export default {
type: String,
required: true,
},
+ hideTextInSmallScreens: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
confidentialTooltip() {
return confidentialityInfoText(this.workspaceType, this.issuableType);
},
+ confidentialTextClass() {
+ return {
+ 'gl-display-none gl-sm-display-block': this.hideTextInSmallScreens,
+ 'gl-ml-2': true,
+ };
+ },
},
};
</script>
<template>
- <gl-badge
- v-gl-tooltip.bottom
- :title="confidentialTooltip"
- icon="eye-slash"
- variant="warning"
- class="gl-display-inline gl-mr-3"
- >{{ __('Confidential') }}</gl-badge
- >
+ <gl-badge v-gl-tooltip :title="confidentialTooltip" variant="warning">
+ <gl-icon name="eye-slash" :size="16" />
+ <span data-testid="confidential-badge-text" :class="confidentialTextClass">{{
+ __('Confidential')
+ }}</span>
+ </gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 65a601ed927..a1ef1f30ebb 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -38,7 +38,16 @@ export default {
default: CONFIRM_DANGER_MODAL_CANCEL,
},
},
+ model: {
+ prop: 'visible',
+ event: 'change',
+ },
props: {
+ visible: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
modalId: {
type: String,
required: true,
@@ -89,12 +98,15 @@ export default {
<template>
<gl-modal
ref="modal"
+ :visible="visible"
:modal-id="modalId"
:data-testid="modalId"
:title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
:action-primary="actionPrimary"
:action-cancel="actionCancel"
+ size="sm"
@primary="$emit('confirm')"
+ @change="$emit('change', $event)"
>
<gl-alert
v-if="confirmDangerMessage"
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
deleted file mode 100644
index d8a2789a419..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue
+++ /dev/null
@@ -1,283 +0,0 @@
-<script>
-import { GlIcon, GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
-import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
-import { __, sprintf } from '~/locale';
-
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import DateTimePickerInput from './date_time_picker_input.vue';
-import {
- defaultTimeRanges,
- defaultTimeRange,
- isValidInputString,
- inputStringToIsoDate,
- isoDateToInputString,
-} from './date_time_picker_lib';
-
-const events = {
- input: 'input',
- invalid: 'invalid',
-};
-
-export default {
- components: {
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlFormGroup,
- TooltipOnTruncate,
- DateTimePickerInput,
- },
- props: {
- value: {
- type: Object,
- required: false,
- default: () => defaultTimeRange,
- },
- options: {
- type: Array,
- required: false,
- default: () => defaultTimeRanges,
- },
- customEnabled: {
- type: Boolean,
- required: false,
- default: true,
- },
- utc: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- timeRange: this.value,
-
- /**
- * Valid start iso date string, null if not valid value
- */
- startDate: null,
- /**
- * Invalid start date string as input by the user
- */
- startFallbackVal: '',
-
- /**
- * Valid end iso date string, null if not valid value
- */
- endDate: null,
- /**
- * Invalid end date string as input by the user
- */
- endFallbackVal: '',
- };
- },
- computed: {
- startInputValid() {
- return isValidInputString(this.startDate);
- },
- endInputValid() {
- return isValidInputString(this.endDate);
- },
- isValid() {
- return this.startInputValid && this.endInputValid;
- },
-
- startInput: {
- get() {
- return this.dateToInput(this.startDate) || this.startFallbackVal;
- },
- set(val) {
- try {
- this.startDate = this.inputToDate(val);
- this.startFallbackVal = null;
- } catch (e) {
- this.startDate = null;
- this.startFallbackVal = val;
- }
- this.timeRange = null;
- },
- },
- endInput: {
- get() {
- return this.dateToInput(this.endDate) || this.endFallbackVal;
- },
- set(val) {
- try {
- this.endDate = this.inputToDate(val);
- this.endFallbackVal = null;
- } catch (e) {
- this.endDate = null;
- this.endFallbackVal = val;
- }
- this.timeRange = null;
- },
- },
-
- timeWindowText() {
- try {
- const timeRange = findTimeRange(this.value, this.options);
- if (timeRange) {
- return timeRange.label;
- }
-
- const { start, end } = convertToFixedRange(this.value);
- if (isValidInputString(start) && isValidInputString(end)) {
- return sprintf(__('%{start} to %{end}'), {
- start: this.stripZerosInDateTime(this.dateToInput(start)),
- end: this.stripZerosInDateTime(this.dateToInput(end)),
- });
- }
- } catch {
- return __('Invalid date range');
- }
- return '';
- },
-
- customLabel() {
- if (this.utc) {
- return __('Custom range (UTC)');
- }
- return __('Custom range');
- },
- },
- watch: {
- value(newValue) {
- const { start, end } = convertToFixedRange(newValue);
- this.timeRange = this.value;
- this.startDate = start;
- this.endDate = end;
- },
- },
- mounted() {
- try {
- const { start, end } = convertToFixedRange(this.timeRange);
- this.startDate = start;
- this.endDate = end;
- } catch {
- // when dates cannot be parsed, emit error.
- this.$emit(events.invalid);
- }
-
- // Validate on mounted, and trigger an update if needed
- if (!this.isValid) {
- this.$emit(events.invalid);
- }
- },
- methods: {
- dateToInput(date) {
- if (date === null) {
- return null;
- }
- return isoDateToInputString(date, this.utc);
- },
- inputToDate(value) {
- return inputStringToIsoDate(value, this.utc);
- },
- stripZerosInDateTime(str = '') {
- return str.replace(' 00:00:00', '');
- },
- closeDropdown() {
- this.$refs.dropdown.hide();
- },
- isOptionActive(option) {
- return isEqualTimeRanges(option, this.timeRange);
- },
- setQuickRange(option) {
- this.timeRange = option;
- this.$emit(events.input, this.timeRange);
- },
- setFixedRange() {
- this.timeRange = convertToFixedRange({
- start: this.startDate,
- end: this.endDate,
- });
- this.$emit(events.input, this.timeRange);
- },
- },
-};
-</script>
-<template>
- <tooltip-on-truncate
- :title="timeWindowText"
- :truncate-target="(elem) => elem.querySelector('.gl-dropdown-toggle-text')"
- placement="top"
- class="d-inline-block"
- >
- <gl-dropdown
- ref="dropdown"
- :text="timeWindowText"
- v-bind="$attrs"
- class="date-time-picker w-100"
- menu-class="date-time-picker-menu"
- toggle-class="date-time-picker-toggle text-truncate"
- >
- <template #button-content>
- <span class="gl-flex-grow-1 text-truncate">{{ timeWindowText }}</span>
- <span v-if="utc" class="gl-text-gray-500 gl-font-weight-bold gl-font-sm">{{
- __('UTC')
- }}</span>
- <gl-icon class="gl-dropdown-caret" name="chevron-down" />
- </template>
-
- <div class="d-flex justify-content-between gl-p-2">
- <gl-form-group
- v-if="customEnabled"
- :label="customLabel"
- label-for="custom-from-time"
- label-class="gl-pb-2"
- class="custom-time-range-form-group col-md-7 gl-pl-2 gl-pr-0 m-0"
- >
- <div class="gl-pt-3">
- <date-time-picker-input
- id="custom-time-from"
- v-model="startInput"
- :label="__('From')"
- :state="startInputValid"
- />
- <date-time-picker-input
- id="custom-time-to"
- v-model="endInput"
- :label="__('To')"
- :state="endInputValid"
- />
- </div>
- <gl-form-group>
- <gl-button data-testid="cancelButton" @click="closeDropdown">{{
- __('Cancel')
- }}</gl-button>
- <gl-button
- variant="confirm"
- category="primary"
- :disabled="!isValid"
- @click="setFixedRange()"
- >
- {{ __('Apply') }}
- </gl-button>
- </gl-form-group>
- </gl-form-group>
- <gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-px-2 m-0">
- <template #label>
- <span class="gl-pl-7">{{ __('Quick range') }}</span>
- </template>
-
- <gl-dropdown-item
- v-for="(option, index) in options"
- :key="index"
- :active="isOptionActive(option)"
- active-class="active"
- @click="setQuickRange(option)"
- >
- <gl-icon
- name="mobile-issue-close"
- class="align-bottom"
- :class="{ invisible: !isOptionActive(option) }"
- />
- {{ option.label }}
- </gl-dropdown-item>
- </gl-form-group>
- </div>
- </gl-dropdown>
- </tooltip-on-truncate>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
deleted file mode 100644
index 190d4e1f104..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_input.vue
+++ /dev/null
@@ -1,77 +0,0 @@
-<script>
-import { GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { __, sprintf } from '~/locale';
-import { dateFormats } from './date_time_picker_lib';
-
-const inputGroupText = {
- invalidFeedback: sprintf(__('Format: %{dateFormat}'), {
- dateFormat: dateFormats.inputFormat,
- }),
- placeholder: dateFormats.inputFormat,
-};
-
-export default {
- components: {
- GlFormGroup,
- GlFormInput,
- },
- props: {
- state: {
- default: null,
- required: true,
- validator: (prop) => typeof prop === 'boolean' || prop === null,
- },
- value: {
- default: null,
- required: false,
- validator: (prop) => typeof prop === 'string' || prop === null,
- },
- label: {
- type: String,
- default: '',
- required: true,
- },
- id: {
- type: String,
- required: false,
- default: () => uniqueId('dateTimePicker_'),
- },
- },
- data() {
- return {
- inputGroupText,
- };
- },
- computed: {
- invalidFeedback() {
- return this.state ? '' : this.inputGroupText.invalidFeedback;
- },
- inputState() {
- // When the state is valid we want to show no
- // green outline. Hence passing null and not true.
- if (this.state === true) {
- return null;
- }
- return this.state;
- },
- },
- methods: {
- onInputBlur(e) {
- this.$emit('input', e.target.value.trim() || null);
- },
- },
-};
-</script>
-
-<template>
- <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback">
- <gl-form-input
- :id="id"
- :value="value"
- :state="inputState"
- :placeholder="inputGroupText.placeholder"
- @blur="onInputBlur"
- />
- </gl-form-group>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
deleted file mode 100644
index 38b1a587b34..00000000000
--- a/app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker_lib.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import dateformat from '~/lib/dateformat';
-import { __ } from '~/locale';
-
-/**
- * Default time ranges for the date picker.
- * @see app/assets/javascripts/lib/utils/datetime_range.js
- */
-export const defaultTimeRanges = [
- {
- duration: { seconds: 60 * 30 },
- label: __('30 minutes'),
- },
- {
- duration: { seconds: 60 * 60 * 3 },
- label: __('3 hours'),
- },
- {
- duration: { seconds: 60 * 60 * 8 },
- label: __('8 hours'),
- default: true,
- },
- {
- duration: { seconds: 60 * 60 * 24 * 1 },
- label: __('1 day'),
- },
-];
-
-export const defaultTimeRange = defaultTimeRanges.find((tr) => tr.default);
-
-export const dateFormats = {
- /**
- * Format used by users to input dates
- *
- * Note: Should be a format that can be parsed by Date.parse.
- */
- inputFormat: 'yyyy-mm-dd HH:MM:ss',
- /**
- * Format used to strip timezone from inputs
- */
- stripTimezoneFormat: "yyyy-mm-dd'T'HH:MM:ss'Z'",
-};
-
-/**
- * Returns true if the date can be parsed succesfully after
- * being typed by a user.
- *
- * It allows some ambiguity so validation is not strict.
- *
- * @param {string} value - Value as typed by the user
- * @returns true if the value can be parsed as a valid date, false otherwise
- */
-export const isValidInputString = (value) => {
- try {
- // dateformat throws error that can be caught.
- // This is better than using `new Date()`
- if (value && value.trim()) {
- dateformat(value, 'isoDateTime');
- return true;
- }
- return false;
- } catch (e) {
- return false;
- }
-};
-
-/**
- * Convert the input in time picker component to an ISO date.
- *
- * @param {string} value
- * @param {Boolean} utc - If true, it forces the date to by
- * formatted using UTC format, ignoring the local time.
- * @returns {Date}
- */
-export const inputStringToIsoDate = (value, utc = false) => {
- let date = new Date(value);
- if (utc) {
- // Forces date to be interpreted as UTC by stripping the timezone
- // by formatting to a string with 'Z' and skipping timezone
- date = dateformat(date, dateFormats.stripTimezoneFormat);
- }
- return dateformat(date, 'isoUtcDateTime');
-};
-
-/**
- * Converts a iso date string to a formatted string for the Time picker component.
- *
- * @param {String} ISO Formatted date
- * @returns {string}
- */
-export const isoDateToInputString = (date, utc = false) =>
- dateformat(date, dateFormats.inputFormat, utc);
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
index 7080e046b30..535f1c5f645 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue
@@ -65,7 +65,8 @@ export default {
viewer() {
if (this.diffViewerMode === diffViewerModes.renamed) {
return RenamedFile;
- } else if (this.diffMode === diffModes.mode_changed) {
+ }
+ if (this.diffMode === diffModes.mode_changed) {
return ModeChanged;
}
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
deleted file mode 100644
index 53210cbcc93..00000000000
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/empty_file.vue
+++ /dev/null
@@ -1,3 +0,0 @@
-<template>
- <div class="nothing-here-block">{{ __('Empty file') }}</div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index 3bb168e9051..b34a6b11092 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -64,6 +64,11 @@ export default {
required: false,
default: undefined,
},
+ noOptionsText: {
+ type: String,
+ required: false,
+ default: __('No options found'),
+ },
},
computed: {
isSearchEmpty() {
@@ -72,6 +77,9 @@ export default {
noOptionsFound() {
return !this.isSearchEmpty && this.options.length === 0;
},
+ noOptions() {
+ return this.isSearchEmpty && this.options.length === 0;
+ },
},
methods: {
selectOption(option) {
@@ -177,6 +185,9 @@ export default {
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
{{ $options.i18n.noMatchingResults }}
</gl-dropdown-item>
+ <gl-dropdown-item v-if="noOptions">
+ {{ noOptionsText }}
+ </gl-dropdown-item>
</template>
</gl-dropdown-form>
</slot>
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 71e3bf4ff63..eb7b20fa4c1 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
@@ -19,6 +19,11 @@ export default {
EntitySelect,
},
props: {
+ apiParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
label: {
type: String,
required: false,
@@ -48,7 +53,7 @@ export default {
default: null,
},
groupsFilter: {
- type: String,
+ type: String, // Two supported values: `descendant_groups` and `subgroups` See app/assets/javascripts/vue_shared/components/entity_select/utils.js.
required: false,
default: null,
},
@@ -62,17 +67,15 @@ export default {
async fetchGroups(searchString = '', page = 1) {
let groups = [];
let totalPages = 0;
+ const params = {
+ search: searchString,
+ per_page: DEFAULT_PER_PAGE,
+ page,
+ ...this.apiParams,
+ };
try {
- const { data = [], headers } = await axios.get(
- Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
- {
- params: {
- search: searchString,
- per_page: DEFAULT_PER_PAGE,
- page,
- },
- },
- );
+ const url = groupsPath(this.groupsFilter, this.parentGroupID);
+ const { data = [], headers } = await axios.get(url, { params });
groups = data.map((group) => ({
...group,
text: group.full_name,
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/utils.js b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
index 0a4622269f4..857a3ab4c74 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/utils.js
+++ b/app/assets/javascripts/vue_shared/components/entity_select/utils.js
@@ -1,15 +1,26 @@
import Api from '~/api';
+/**
+ * @param {'descendant_groups'|'subgroups'|null} [groupsFilter] - type of group filtering
+ * @param {string|null} [parentGroupID] - parent group is needed for 'descendant_groups' and 'subgroups' filtering.
+ */
export const groupsPath = (groupsFilter, parentGroupID) => {
- if (groupsFilter !== undefined && parentGroupID === undefined) {
+ if (groupsFilter && !parentGroupID) {
throw new Error('Cannot use groupsFilter without a parentGroupID');
}
+
+ let url = '';
switch (groupsFilter) {
case 'descendant_groups':
- return Api.descendantGroupsPath.replace(':id', parentGroupID);
+ url = Api.descendantGroupsPath.replace(':id', parentGroupID);
+ break;
case 'subgroups':
- return Api.subgroupsPath.replace(':id', parentGroupID);
+ url = Api.subgroupsPath.replace(':id', parentGroupID);
+ break;
default:
- return Api.groupsPath;
+ url = Api.groupsPath;
+ break;
}
+
+ return Api.buildUrl(url);
};
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index db0b0ea185b..226f44a1541 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -145,7 +145,8 @@ export default {
el.classList.contains('inputarea')
) {
return true;
- } else if (combo === 'mod+p') {
+ }
+ if (combo === 'mod+p') {
return false;
}
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 721f87ff4d6..cecd1be82e9 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -141,7 +141,6 @@ export default {
ref="textOutput"
class="file-row-name"
:title="file.name"
- data-qa-selector="file_name_content"
:data-qa-file-name="file.name"
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 2b3d1b2c1f5..c698b94749d 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -73,6 +73,7 @@ export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_REVIEWER = s__('SearchToken|Reviewer');
export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
export const TOKEN_TITLE_STATUS = __('Status');
+export const TOKEN_TITLE_JOBS_RUNNER_TYPE = s__('Job|Runner type');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
@@ -100,6 +101,7 @@ export const TOKEN_TYPE_RELEASE = 'release';
export const TOKEN_TYPE_REVIEWER = 'reviewer';
export const TOKEN_TYPE_SOURCE_BRANCH = 'source-branch';
export const TOKEN_TYPE_STATUS = 'status';
+export const TOKEN_TYPE_JOBS_RUNNER_TYPE = 'jobs-runner-type';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
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 f31d4d53a23..346384e3023 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
@@ -73,7 +73,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
- required: true,
+ required: false,
+ default: __('Search or filter results…'),
},
suggestionsListClass: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 8322fe92de4..77108ad3628 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -2,6 +2,8 @@
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+import searchMilestonesQuery from '~/issues/list/queries/search_milestones.query.graphql';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { stripQuotes } from '~/lib/utils/text_utility';
@@ -36,6 +38,14 @@ export default {
defaultMilestones() {
return this.config.defaultMilestones || DEFAULT_MILESTONES;
},
+ namespace() {
+ return this.config.isProject ? WORKSPACE_PROJECT : WORKSPACE_GROUP;
+ },
+ fetchMilestonesQuery() {
+ return this.config.fetchMilestones
+ ? this.config.fetchMilestones
+ : this.fetchMilestonesBySearchTerm;
+ },
},
methods: {
getActiveMilestone(milestones, data) {
@@ -51,10 +61,17 @@ export default {
) || this.defaultMilestones.find(({ value }) => value === data)
);
},
+ fetchMilestonesBySearchTerm(search) {
+ return this.$apollo
+ .query({
+ query: searchMilestonesQuery,
+ variables: { fullPath: this.config.fullPath, search, isProject: this.config.isProject },
+ })
+ .then(({ data }) => data[this.namespace]?.milestones.nodes);
+ },
fetchMilestones(searchTerm) {
this.loading = true;
- this.config
- .fetchMilestones(searchTerm)
+ this.fetchMilestonesQuery(searchTerm)
.then((response) => {
const data = Array.isArray(response) ? response : response.data;
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
index 7da45169fee..a375a167c68 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
@@ -24,6 +24,7 @@ export default {
:key="group.id"
:group="group"
:show-group-icon="showGroupIcon"
+ @delete="$emit('delete', $event)"
/>
</ul>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index 8a301cd0dd0..ca1e7400f2d 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
+import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
@@ -7,6 +8,9 @@ import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import DangerConfirmModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
export default {
i18n: {
@@ -25,6 +29,8 @@ export default {
GlIcon,
UserAccessRoleBadge,
GlTruncateText,
+ ListActions,
+ DangerConfirmModal,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -41,6 +47,12 @@ export default {
default: false,
},
},
+ data() {
+ return {
+ isDeleteModalVisible: false,
+ modalId: uniqueId('groups-list-item-modal-id-'),
+ };
+ },
computed: {
visibility() {
return this.group.visibility;
@@ -75,94 +87,131 @@ export default {
groupMembersCount() {
return numberToMetricPrefix(this.group.groupMembersCount);
},
+ actions() {
+ return {
+ [ACTION_EDIT]: {
+ href: this.group.editPath,
+ },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
+ },
+ };
+ },
+ hasActions() {
+ return this.group.availableActions?.length;
+ },
+ hasActionDelete() {
+ return this.group.availableActions?.includes(ACTION_DELETE);
+ },
+ },
+ methods: {
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
<template>
- <li class="groups-list-item gl-py-5 gl-md-display-flex gl-align-items-center gl-border-b">
- <div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
- v-if="showGroupIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- :name="groupIconName"
- />
- <gl-avatar-labeled
- :entity-id="group.id"
- :entity-name="group.fullName"
- :label="group.fullName"
- :label-link="group.webUrl"
- shape="rect"
- :size="$options.avatarSize"
- >
- <template #meta>
- <div class="gl-px-2">
- <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap">
- <div class="gl-px-2">
- <gl-icon
- v-if="visibility"
- v-gl-tooltip="visibilityTooltip"
- :name="visibilityIcon"
- class="gl-text-secondary"
- />
- </div>
- <div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
- accessLevelLabel
- }}</user-access-role-badge>
+ <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
+ <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <div class="gl-display-flex gl-flex-grow-1">
+ <gl-icon
+ v-if="showGroupIcon"
+ class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
+ :name="groupIconName"
+ />
+ <gl-avatar-labeled
+ :entity-id="group.id"
+ :entity-name="group.fullName"
+ :label="group.fullName"
+ :label-link="group.webUrl"
+ shape="rect"
+ :size="$options.avatarSize"
+ >
+ <template #meta>
+ <div class="gl-px-2">
+ <div class="gl-mx-n2 gl-display-flex gl-align-items-center gl-flex-wrap">
+ <div class="gl-px-2">
+ <gl-icon
+ v-if="visibility"
+ v-gl-tooltip="visibilityTooltip"
+ :name="visibilityIcon"
+ class="gl-text-secondary"
+ />
+ </div>
+ <div class="gl-px-2">
+ <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ accessLevelLabel
+ }}</user-access-role-badge>
+ </div>
</div>
</div>
+ </template>
+ <gl-truncate-text
+ v-if="group.descriptionHtml"
+ :lines="2"
+ :mobile-lines="2"
+ :show-more-text="$options.i18n.showMore"
+ :show-less-text="$options.i18n.showLess"
+ class="gl-mt-2"
+ >
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
+ class="gl-font-sm md"
+ data-testid="group-description"
+ ></div>
+ </gl-truncate-text>
+ </gl-avatar-labeled>
+ </div>
+ <div
+ class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
+ :class="statsPadding"
+ >
+ <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
+ <div
+ v-gl-tooltip="$options.i18n.subgroups"
+ :aria-label="$options.i18n.subgroups"
+ class="gl-text-secondary"
+ data-testid="subgroups-count"
+ >
+ <gl-icon name="subgroup" />
+ <span>{{ descendantGroupsCount }}</span>
</div>
- </template>
- <gl-truncate-text
- v-if="group.descriptionHtml"
- :lines="2"
- :mobile-lines="2"
- :show-more-text="$options.i18n.showMore"
- :show-less-text="$options.i18n.showLess"
- class="gl-mt-2"
- >
<div
- v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
- class="gl-font-sm md"
- data-testid="group-description"
- ></div>
- </gl-truncate-text>
- </gl-avatar-labeled>
- </div>
- <div
- class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
- :class="statsPadding"
- >
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
- <div
- v-gl-tooltip="$options.i18n.subgroups"
- :aria-label="$options.i18n.subgroups"
- class="gl-text-secondary"
- data-testid="subgroups-count"
- >
- <gl-icon name="subgroup" />
- <span>{{ descendantGroupsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.projects"
- :aria-label="$options.i18n.projects"
- class="gl-text-secondary"
- data-testid="projects-count"
- >
- <gl-icon name="project" />
- <span>{{ projectsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.directMembers"
- :aria-label="$options.i18n.directMembers"
- class="gl-text-secondary"
- data-testid="members-count"
- >
- <gl-icon name="users" />
- <span>{{ groupMembersCount }}</span>
+ v-gl-tooltip="$options.i18n.projects"
+ :aria-label="$options.i18n.projects"
+ class="gl-text-secondary"
+ data-testid="projects-count"
+ >
+ <gl-icon name="project" />
+ <span>{{ projectsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.directMembers"
+ :aria-label="$options.i18n.directMembers"
+ class="gl-text-secondary"
+ data-testid="members-count"
+ >
+ <gl-icon name="users" />
+ <span>{{ groupMembersCount }}</span>
+ </div>
</div>
</div>
</div>
+ <list-actions
+ v-if="hasActions"
+ class="gl-ml-3 gl-md-align-self-center"
+ :actions="actions"
+ :available-actions="group.availableActions"
+ />
+
+ <danger-confirm-modal
+ v-if="hasActionDelete"
+ v-model="isDeleteModalVisible"
+ :modal-id="modalId"
+ :phrase="group.fullName"
+ @confirm="$emit('delete', group)"
+ />
</li>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/incidents/utils.js b/app/assets/javascripts/vue_shared/components/incidents/utils.js
deleted file mode 100644
index bcb578a6ba6..00000000000
--- a/app/assets/javascripts/vue_shared/components/incidents/utils.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { noop } from 'lodash';
-
-export const isValidSlaDueAt = noop;
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/constants.js b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
new file mode 100644
index 00000000000..b1506ae1e93
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
@@ -0,0 +1,16 @@
+import { __ } from '~/locale';
+
+export const ACTION_EDIT = 'edit';
+export const ACTION_DELETE = 'delete';
+
+export const BASE_ACTIONS = {
+ [ACTION_EDIT]: {
+ text: __('Edit'),
+ },
+ [ACTION_DELETE]: {
+ text: __('Delete'),
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
new file mode 100644
index 00000000000..d34729c2373
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
@@ -0,0 +1,44 @@
+import { makeContainer } from 'storybook_addons/make_container';
+import ListActions from './list_actions.vue';
+import { ACTION_DELETE, ACTION_EDIT } from './constants';
+
+export default {
+ component: ListActions,
+ title: 'vue_shared/list_actions',
+ decorators: [makeContainer({ height: '115px' })],
+ parameters: {
+ docs: {
+ description: {
+ component: `
+This component renders actions used by lists of resources such as groups and projects.
+Currently it is used by \`ProjectsListItem\`. There are base actions defined in \`~/vue_shared/components/list_actions\`
+that help reduce the amount of boilerplate needed for common actions such as edit and delete. This component accepts an
+\`actions\` prop that can extend the base actions and/or add custom actions. These actions should follow the format of
+a [disclosure dropdown item](https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items).
+The \`availableActions\` prop defines what actions to render and in what order. This prop will generally be set by checking
+permissions of the current user.
+`,
+ },
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ListActions },
+ props: Object.keys(argTypes),
+ template: '<list-actions v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ actions: {
+ [ACTION_EDIT]: {
+ href: '/?path=/story/vue-shared-list-actions--default',
+ },
+ [ACTION_DELETE]: {
+ // eslint-disable-next-line no-console
+ action: () => console.log('Deleted'),
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
new file mode 100644
index 00000000000..7b78cc1da8f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BASE_ACTIONS } from './constants';
+
+export default {
+ name: 'ListActions',
+ i18n: {
+ actions: __('Actions'),
+ },
+ components: {
+ GlDisclosureDropdown,
+ },
+ props: {
+ // Can extend `BASE_ACTIONS` and/or add new actions.
+ // Expected format: https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items
+ actions: {
+ type: Object,
+ required: true,
+ },
+ availableActions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ items() {
+ return this.availableActions.reduce((accumulator, action) => {
+ return [
+ ...accumulator,
+ {
+ ...BASE_ACTIONS[action],
+ ...this.actions[action],
+ },
+ ];
+ }, []);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ :items="items"
+ icon="ellipsis_v"
+ no-caret
+ :toggle-text="$options.i18n.actions"
+ text-sr-only
+ placement="right"
+ category="tertiary"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index a570abae9d3..05ce007e615 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -1,9 +1,9 @@
<script>
-import { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlForm, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import { __, n__ } from '~/locale';
export default {
- components: { GlDropdown, GlDropdownForm, GlFormTextarea, GlButton, GlAlert },
+ components: { GlDisclosureDropdown, GlForm, GlFormTextarea, GlButton, GlAlert },
props: {
disabled: {
type: Boolean,
@@ -39,43 +39,58 @@ export default {
return n__('Apply %d suggestion', 'Apply %d suggestions', this.batchSuggestionsCount);
},
+ helperText() {
+ if (this.batchSuggestionsCount <= 1) {
+ return __('This also resolves this thread');
+ }
+
+ return __('This also resolves all related threads');
+ },
},
methods: {
onApply() {
this.$emit('apply', this.message);
},
+ focusCommitMessageInput() {
+ this.$refs.commitMessage.$el.focus();
+ },
},
};
</script>
<template>
- <gl-dropdown
- :text="dropdownText"
- :disabled="disabled"
- size="small"
- boundary="window"
- right
- lazy
- menu-class="gl-w-full!"
+ <gl-disclosure-dropdown
data-qa-selector="apply_suggestion_dropdown"
- @shown="$refs.commitMessage.$el.focus()"
+ fluid-width
+ placement="right"
+ size="small"
+ :disabled="disabled"
+ :toggle-text="dropdownText"
+ @shown="focusCommitMessageInput"
>
- <gl-dropdown-form class="gl-px-4! gl-m-0!">
+ <gl-form class="gl-display-flex gl-flex-direction-column gl-px-4! gl-mx-0! gl-my-2!">
<label for="commit-message">{{ __('Commit message') }}</label>
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-4">
{{ errorMessage }}
</gl-alert>
+
<gl-form-textarea
id="commit-message"
ref="commitMessage"
v-model="message"
+ class="apply-suggestions-input-min-width"
:placeholder="defaultCommitMessage"
submit-on-enter
data-qa-selector="commit_message_field"
@submit="onApply"
/>
+
+ <span class="gl-mt-2 gl-text-secondary">
+ {{ helperText }}
+ </span>
+
<gl-button
- class="gl-w-auto! gl-mt-3 gl-text-center! gl-transition-medium! float-right"
+ class="gl-w-auto! gl-mt-3 gl-align-self-end"
category="primary"
variant="confirm"
data-qa-selector="commit_with_custom_message_button"
@@ -83,6 +98,6 @@ export default {
>
{{ __('Apply') }}
</gl-button>
- </gl-dropdown-form>
- </gl-dropdown>
+ </gl-form>
+ </gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index b1c6f5e6056..f7f5ccdbf31 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -4,7 +4,11 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
import { InternalEvents } from '~/tracking';
import savedRepliesQuery from './saved_replies.query.graphql';
-import { TRACKING_SAVED_REPLIES_USE, TRACKING_SAVED_REPLIES_USE_IN_MR } from './constants';
+import {
+ TRACKING_SAVED_REPLIES_USE,
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+} from './constants';
export default {
apollo: {
@@ -61,9 +65,9 @@ export default {
if (savedReply) {
this.$emit('select', savedReply.content);
this.track_event(TRACKING_SAVED_REPLIES_USE);
- if (isInMr) {
- this.track_event(TRACKING_SAVED_REPLIES_USE_IN_MR);
- }
+ this.track_event(
+ isInMr ? TRACKING_SAVED_REPLIES_USE_IN_MR : TRACKING_SAVED_REPLIES_USE_IN_OTHER,
+ );
}
},
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/constants.js b/app/assets/javascripts/vue_shared/components/markdown/constants.js
index 47ef7cccbc2..7b31c4a59e3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/constants.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/constants.js
@@ -1,2 +1,3 @@
export const TRACKING_SAVED_REPLIES_USE = 'i_code_review_saved_replies_use';
export const TRACKING_SAVED_REPLIES_USE_IN_MR = 'i_code_review_saved_replies_use_in_mr';
+export const TRACKING_SAVED_REPLIES_USE_IN_OTHER = 'i_code_review_saved_replies_use_in_other';
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
index 84d40db07bb..c70197c6715 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
@@ -2,8 +2,26 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ watch: {
+ isLoading() {
+ this.handleGFM();
+ },
+ },
mounted() {
- renderGFM(this.$el);
+ this.handleGFM();
+ },
+ methods: {
+ handleGFM() {
+ if (this.isLoading) return;
+ renderGFM(this.$el);
+ },
},
};
</script>
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 493b329f1b1..fc7e0a7c732 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -119,9 +119,11 @@ export default {
},
},
data() {
+ const editingMode =
+ localStorage.getItem(this.$options.EDITING_MODE_KEY) || EDITING_MODE_MARKDOWN_FIELD;
return {
markdown: this.value || (this.autosaveKey ? getDraft(this.autosaveKey) : '') || '',
- editingMode: EDITING_MODE_MARKDOWN_FIELD,
+ editingMode,
autofocused: false,
};
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
index 0b0867ae84c..6c2f084591e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
+++ b/app/assets/javascripts/vue_shared/components/markdown/mount_markdown_editor.js
@@ -72,7 +72,7 @@ export function mountMarkdownEditor(options = {}) {
quickActionsDocsPath,
formFieldPlaceholder,
formFieldClasses,
- qaSelector,
+ testid,
newIssuePath,
} = el.dataset;
@@ -115,7 +115,7 @@ export function mountMarkdownEditor(options = {}) {
id: formFieldId,
name: formFieldName,
class: formFieldClasses,
- 'data-qa-selector': qaSelector,
+ 'data-testid': testid,
},
autosaveKey,
enableAutocomplete,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 855c7a449c4..8a0ca8ebac1 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -77,9 +77,7 @@ export default {
return this.inapplicableReason;
}
- return this.batchSuggestionsCount > 1
- ? __('This also resolves all related threads')
- : __('This also resolves this thread');
+ return false;
},
isDisableButton() {
return this.isApplying || !this.canApply;
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 9179331cdec..0ec8b6e2a0a 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -6,6 +6,9 @@ const noteableTypeText = {
Issue: __('issue'),
Epic: __('epic'),
MergeRequest: __('merge request'),
+ Task: __('task'),
+ KeyResult: __('key result'),
+ Objective: __('objective'),
};
export default {
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
index df1188d365b..77fd197978f 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
@@ -15,7 +13,3 @@ export const initialPaginationState = {
firstPageSize: defaultPageSize,
lastPageSize: null,
};
-
-export const defaultI18n = {
- searchPlaceholder: __('Search or filter results…'),
-};
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index ab9e6e092d9..0c3d175684c 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -14,11 +14,10 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
-import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
+import { initialPaginationState, defaultPageSize } from './constants';
import { isAny } from './utils';
export default {
- defaultI18n,
components: {
GlAlert,
GlBadge,
@@ -300,7 +299,6 @@ export default {
<div class="filtered-search-wrapper">
<filtered-search-bar
:namespace="projectPath"
- :search-input-placeholder="$options.defaultI18n.searchPlaceholder"
:tokens="filteredSearchTokens"
:initial-filter-value="filteredSearchValue"
initial-sortby="created_desc"
diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index e1f042f78ab..76bedc0feeb 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
@@ -64,7 +64,7 @@ export default {
<template>
<gl-pagination
v-if="showPagination"
- class="gl-mt-3"
+ class="gl-mt-5"
v-bind="$attrs"
align="center"
:value="pageInfo.page"
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
index c1246b2bf44..4f580d4a848 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -1,5 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlIcon,
+ GlSprintf,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -9,8 +15,9 @@ const DEFAULT_PAGE_SIZES = [20, 50, 100];
export default {
components: {
PaginationLinks,
- GlDropdown,
- GlDropdownItem,
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlIcon,
GlSprintf,
LocalStorageSync,
@@ -80,25 +87,31 @@ export default {
@input="setPageSize"
/>
<pagination-links :change="setPage" :page-info="pageInfo" class="gl-m-0" />
- <gl-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
- <template #button-content>
- <span class="gl-font-weight-bold">
+ <gl-disclosure-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
+ <template #toggle>
+ <gl-button class="gl-font-weight-bold" category="tertiary">
<gl-sprintf :message="__('%{count} items per page')">
<template #count>
{{ pageInfo.perPage }}
</template>
</gl-sprintf>
- </span>
- <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
+ <gl-icon class="gl-button-icon dropdown-chevron" name="chevron-down" />
+ </gl-button>
</template>
- <gl-dropdown-item v-for="size in pageSizes" :key="size" @click="setPageSize(size)">
- <gl-sprintf :message="__('%{count} items per page')">
- <template #count>
- {{ size }}
- </template>
- </gl-sprintf>
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-disclosure-dropdown-item
+ v-for="size in pageSizes"
+ :key="size"
+ @action="setPageSize(size)"
+ >
+ <template #list-item>
+ <gl-sprintf :message="__('%{count} items per page')">
+ <template #count>
+ {{ size }}
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<div class="gl-ml-2" data-testid="information">
<gl-sprintf :message="s__('BulkImport|Showing %{start}-%{end} of %{total}')">
<template #start>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/constants.js b/app/assets/javascripts/vue_shared/components/projects_list/constants.js
deleted file mode 100644
index aa0b1418a06..00000000000
--- a/app/assets/javascripts/vue_shared/components/projects_list/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const ACTION_EDIT = 'edit';
-export const ACTION_DELETE = 'delete';
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 9fc4571b0dc..ce75e305473 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -7,7 +7,6 @@ import {
GlTooltipDirective,
GlPopover,
GlSprintf,
- GlDisclosureDropdown,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
@@ -20,8 +19,9 @@ import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
import SafeHtml from '~/vue_shared/directives/safe_html';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
-import { ACTION_EDIT, ACTION_DELETE } from './constants';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
@@ -51,8 +51,8 @@ export default {
GlPopover,
GlSprintf,
TimeAgoTooltip,
- GlDisclosureDropdown,
DeleteModal,
+ ListActions,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -163,30 +163,21 @@ export default {
return numberToMetricPrefix(this.project.openIssuesCount);
},
- actionsDropdownItems() {
- return [
- {
- id: ACTION_EDIT,
- text: __('Edit'),
+ actions() {
+ return {
+ [ACTION_EDIT]: {
href: this.project.editPath,
},
- {
- id: ACTION_DELETE,
- text: __('Delete'),
- extraAttrs: {
- class: 'gl-text-red-500!',
- },
- action: () => {
- this.isDeleteModalVisible = true;
- },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
},
- ].filter(({ id }) => this.project.actions?.includes(id));
+ };
},
hasActions() {
- return this.actionsDropdownItems.length;
+ return this.project.availableActions?.length;
},
- hasDeleteAction() {
- return this.actionsDropdownItems.find((action) => action.id === ACTION_DELETE);
+ hasActionDelete() {
+ return this.project.availableActions?.includes(ACTION_DELETE);
},
},
methods: {
@@ -204,6 +195,9 @@ export default {
return null;
},
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
@@ -336,20 +330,15 @@ export default {
</div>
</div>
</div>
- <gl-disclosure-dropdown
+ <list-actions
v-if="hasActions"
class="gl-ml-3 gl-md-align-self-center"
- :items="actionsDropdownItems"
- icon="ellipsis_v"
- no-caret
- :toggle-text="$options.i18n.actions"
- text-sr-only
- placement="right"
- category="tertiary"
+ :actions="actions"
+ :available-actions="project.availableActions"
/>
<delete-modal
- v-if="hasDeleteAction"
+ v-if="hasActionDelete"
v-model="isDeleteModalVisible"
:confirm-phrase="project.name"
:is-fork="project.isForked"
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index 7b7d3d48d9e..53c16fccba1 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -104,7 +104,7 @@ export default {
:id="`source-editor-${fileGlobalId}`"
ref="editor"
data-editor-loading
- data-qa-selector="source_editor_container"
+ data-testid="source-editor-container"
@[$options.readyEvent]="$emit($options.readyEvent, $event)"
>
<pre class="editor-loading-content">{{ value }}</pre>
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 b89fa3f8292..8dac6327a99 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
@@ -82,6 +82,7 @@ export default {
methods: {
handleChunkAppear() {
this.hasAppeared = true;
+ this.$emit('appear');
},
calculateLineNumber(index) {
return this.startingFrom + index + 1;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
index a4d50466f8f..797a38d8171 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue
@@ -33,6 +33,7 @@ export default {
components: {
GlLoadingIcon,
Chunk,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
mixins: [Tracking.mixin()],
props: {
@@ -40,6 +41,14 @@ export default {
type: Object,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ currentRef: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -49,7 +58,6 @@ export default {
firstChunk: null,
chunks: {},
isLoading: true,
- isLineSelected: false,
lineHighlighter: null,
};
},
@@ -66,7 +74,8 @@ export default {
if (this.blob.name && this.blob.name.endsWith(`.${SVELTE_LANGUAGE}`)) {
// override for svelte files until https://github.com/rouge-ruby/rouge/issues/1717 is resolved
return SVELTE_LANGUAGE;
- } else if (this.blob.name === this.$options.codeownersFileName) {
+ }
+ if (this.isCodeownersFile) {
// override for codeowners files
return this.$options.codeownersLanguage;
}
@@ -87,6 +96,9 @@ export default {
totalChunks() {
return Object.keys(this.chunks).length;
},
+ isCodeownersFile() {
+ return this.blob.name === CODEOWNERS_FILE_NAME;
+ },
},
async created() {
if (this.isLfsBlob) {
@@ -121,7 +133,7 @@ export default {
this.generateRemainingChunks();
this.isLoading = false;
await this.$nextTick();
- this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ this.selectLine();
});
},
methods: {
@@ -227,18 +239,16 @@ export default {
return languageDefinition;
},
async selectLine() {
- if (this.isLineSelected || !this.lineHighlighter) {
- return;
+ if (!this.lineHighlighter) {
+ this.lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
}
-
- this.isLineSelected = true;
await this.$nextTick();
- this.lineHighlighter.highlightHash(this.$route.hash);
+ const scrollEnabled = false;
+ this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
},
},
userColorScheme: window.gon.user_color_scheme,
currentlySelectedLine: null,
- codeownersFileName: CODEOWNERS_FILE_NAME,
codeownersLanguage: CODEOWNERS_LANGUAGE,
};
</script>
@@ -250,6 +260,13 @@ export default {
:data-path="blob.path"
data-qa-selector="blob_viewer_file_content"
>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ class="gl-text-black-normal"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="blob.path"
+ />
<chunk
v-if="firstChunk"
:lines="firstChunk.lines"
@@ -263,20 +280,21 @@ export default {
/>
<gl-loading-icon v-if="isLoading" size="sm" class="gl-my-5" />
- <chunk
- v-for="(chunk, key, index) in chunks"
- v-else
- :key="key"
- :lines="chunk.lines"
- :content="chunk.content"
- :total-lines="chunk.totalLines"
- :starting-from="chunk.startingFrom"
- :is-highlighted="chunk.isHighlighted"
- :chunk-index="index"
- :language="chunk.language"
- :blame-path="blob.blamePath"
- :total-chunks="totalChunks"
- @appear="highlightChunk"
- />
+ <template v-else>
+ <chunk
+ v-for="(chunk, key, index) in chunks"
+ :key="key"
+ :lines="chunk.lines"
+ :content="chunk.content"
+ :total-lines="chunk.totalLines"
+ :starting-from="chunk.startingFrom"
+ :is-highlighted="chunk.isHighlighted"
+ :chunk-index="index"
+ :language="chunk.language"
+ :blame-path="blob.blamePath"
+ :total-chunks="totalChunks"
+ @appear="highlightChunk"
+ />
+ </template>
</div>
</template>
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 0fb6e577f32..c7353ed6785 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
@@ -41,8 +41,14 @@ export default {
addBlobLinksTracking();
},
mounted() {
- const { hash } = this.$route;
- this.lineHighlighter.highlightHash(hash);
+ this.selectLine();
+ },
+ methods: {
+ async selectLine() {
+ await this.$nextTick();
+ const scrollEnabled = false;
+ this.lineHighlighter.highlightHash(this.$route.hash, scrollEnabled);
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -66,6 +72,7 @@ export default {
:total-lines="chunk.totalLines"
:starting-from="chunk.startingFrom"
:blame-path="blob.blamePath"
+ @appear="selectLine"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/split_button.vue b/app/assets/javascripts/vue_shared/components/split_button.vue
deleted file mode 100644
index c0aef42b0f2..00000000000
--- a/app/assets/javascripts/vue_shared/components/split_button.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownDivider, GlDropdownItem } from '@gitlab/ui';
-import { isString } from 'lodash';
-
-const isValidItem = (item) =>
- isString(item.eventName) && isString(item.title) && isString(item.description);
-
-export default {
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- },
-
- props: {
- actionItems: {
- type: Array,
- required: true,
- validator(value) {
- return value.length > 1 && value.every(isValidItem);
- },
- },
- menuClass: {
- type: String,
- required: false,
- default: '',
- },
- variant: {
- type: String,
- required: false,
- default: 'default',
- },
- },
-
- data() {
- return {
- selectedItem: this.actionItems[0],
- };
- },
-
- computed: {
- dropdownToggleText() {
- return this.selectedItem.title;
- },
- },
-
- methods: {
- triggerEvent() {
- this.$emit(this.selectedItem.eventName);
- },
- changeSelectedItem(item) {
- this.selectedItem = item;
- this.$emit('change', item);
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- :menu-class="menuClass"
- split
- :text="dropdownToggleText"
- :variant="variant"
- v-bind="$attrs"
- @click="triggerEvent"
- >
- <template v-for="(item, itemIndex) in actionItems">
- <gl-dropdown-item
- :key="item.eventName"
- is-check-item
- :is-checked="selectedItem === item"
- @click="changeSelectedItem(item)"
- >
- <strong>{{ item.title }}</strong>
- <div>{{ item.description }}</div>
- </gl-dropdown-item>
-
- <gl-dropdown-divider
- v-if="itemIndex < actionItems.length - 1"
- :key="`${item.eventName}-divider`"
- />
- </template>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
index bda88a48e48..9ba5e8724f9 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue
@@ -70,7 +70,8 @@ export default {
selectTarget() {
if (isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el);
- } else if (this.truncateTarget === 'child') {
+ }
+ if (this.truncateTarget === 'child') {
return this.$el.childNodes[0];
}
return this.$el;
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 446c8c97df0..30f616dd8e1 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -83,9 +83,11 @@ export default {
if (this.user.status.emoji && this.user.status.message_html) {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
- } else if (this.user.status.message_html) {
+ }
+ if (this.user.status.message_html) {
return this.user.status.message_html;
- } else if (this.user.status.emoji) {
+ }
+ if (this.user.status.emoji) {
return glEmojiTag(this.user.status.emoji);
}
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 4879baced0d..863c43b0e55 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -13,7 +13,7 @@ import { __ } from '~/locale';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { participantsQueries, userSearchQueries } from '~/sidebar/constants';
+import { participantsQueries, userSearchQueries } from '~/sidebar/queries/constants';
import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -130,11 +130,11 @@ export default {
},
update(data) {
return (
- data.workspace?.users?.nodes
- .filter((x) => x?.user)
- .map((node) => ({
- ...node.user,
- canMerge: node.mergeRequestInteraction?.canMerge || false,
+ data.workspace?.users
+ .filter((user) => user)
+ .map((user) => ({
+ ...user,
+ canMerge: user.mergeRequestInteraction?.canMerge || false,
})) || []
);
},
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 79d14b5f2d0..beb8321a271 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -195,9 +195,11 @@ export default {
webIdeActionText() {
if (this.webIdeText) {
return this.webIdeText;
- } else if (this.isBlob) {
+ }
+ if (this.isBlob) {
return __('Open in Web IDE');
- } else if (this.isFork) {
+ }
+ if (this.isFork) {
return __('Edit fork in Web IDE');
}
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index d9bc2c82688..9c001fa2e9a 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -86,7 +86,7 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
),
{
workspaceType: workspaceType === WORKSPACE_PROJECT ? __('project') : __('group'),
- issuableType: issuableType.toLowerCase(),
+ issuableType: issuableType.toLowerCase().replaceAll('_', ' '),
permissions:
issuableType === TYPE_ISSUE
? __('at least the Reporter role, the author, and assignees')
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
index 699b41f3bf3..1cfa3f6d3d7 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -36,7 +36,7 @@ export default {
ariaLabel: __('Description'),
class: 'rspec-issuable-form-description',
placeholder: __('Write a comment or drag your files here…'),
- dataQaSelector: 'issuable_form_description_field',
+ dataTestid: 'issuable-form-description-field',
id: 'issuable-description',
name: 'issuable-description',
},
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 31dd49ca415..690d9523a63 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -8,6 +8,7 @@ import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import { STATE_CLOSED } from '~/work_items/constants';
import { isAssigneesWidget, isLabelsWidget } from '~/work_items/utils';
@@ -24,6 +25,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml,
},
mixins: [timeagoMixin],
props: {
@@ -80,14 +82,20 @@ export default {
author() {
return this.issuable.author || {};
},
+ externalAuthor() {
+ return this.issuable.externalAuthor;
+ },
webUrl() {
return this.issuable.gitlabWebUrl || this.issuable.webUrl;
},
authorId() {
return getIdFromGraphQLId(this.author.id);
},
+ isIssueTrackerExternal() {
+ return Boolean(this.issuable.externalTracker);
+ },
isIssuableUrlExternal() {
- return isExternal(this.webUrl);
+ return isExternal(this.webUrl ?? '');
},
reference() {
return this.issuable.reference || `${this.issuableSymbol}${this.issuable.iid}`;
@@ -130,7 +138,8 @@ export default {
return sprintf(__('closed %{timeago}'), {
timeago: this.timeFormatted(this.issuable.closedAt),
});
- } else if (this.issuable.updatedAt !== this.issuable.createdAt) {
+ }
+ if (this.issuable.updatedAt !== this.issuable.createdAt) {
return sprintf(__('updated %{timeAgo}'), {
timeAgo: this.timeFormatted(this.issuable.updatedAt),
});
@@ -242,6 +251,7 @@ export default {
<div data-testid="issuable-title" class="issue-title title">
<work-item-type-icon
v-if="showWorkItemTypeIcon"
+ class="gl-mr-2"
:work-item-type="type"
show-tooltip-on-hover
/>
@@ -259,18 +269,33 @@ export default {
:title="__('This issue is hidden because its author has been banned')"
:aria-label="__('Hidden')"
/>
- <gl-link
- class="issue-title-text"
- dir="auto"
- :href="webUrl"
- data-qa-selector="issuable_title_link"
- data-testid="issuable-title-link"
- v-bind="issuableTitleProps"
- @click="handleIssuableItemClick"
- >
- {{ issuable.title }}
+ <template v-if="isIssueTrackerExternal">
+ <gl-link
+ class="issue-title-text"
+ dir="auto"
+ :href="webUrl"
+ data-qa-selector="issuable_title_link"
+ data-testid="issuable-title-link"
+ v-bind="issuableTitleProps"
+ @click="handleIssuableItemClick"
+ >
+ {{ issuable.title }}
+ <gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
+ </gl-link>
+ </template>
+ <template v-else>
+ <gl-link
+ v-safe-html="issuable.titleHtml || issuable.title"
+ class="issue-title-text"
+ dir="auto"
+ :href="webUrl"
+ data-qa-selector="issuable_title_link"
+ data-testid="issuable-title-link"
+ v-bind="issuableTitleProps"
+ @click="handleIssuableItemClick"
+ />
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
- </gl-link>
+ </template>
<span
v-if="taskStatus"
class="task-status gl-display-none gl-sm-display-inline-block! gl-ml-2 gl-font-sm"
@@ -298,6 +323,9 @@ export default {
</span>
</template>
<template #author>
+ <span v-if="externalAuthor" data-testid="external-author"
+ >{{ externalAuthor }} {{ __('via') }}</span
+ >
<slot v-if="hasSlotContents('author')" name="author"></slot>
<gl-link
v-else
@@ -344,7 +372,7 @@ export default {
</div>
<div class="issuable-meta">
<ul v-if="showIssuableMeta" class="controls">
- <li v-if="hasSlotContents('status')" class="issuable-status">
+ <li v-if="hasSlotContents('status')">
<slot name="status"></slot>
</li>
<li v-if="assignees.length">
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 7a9404e06c7..0db7417cebc 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -5,6 +5,7 @@ import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -51,7 +52,8 @@ export default {
},
searchInputPlaceholder: {
type: String,
- required: true,
+ required: false,
+ default: __('Search or filter results…'),
},
searchTokens: {
type: Array,
@@ -344,7 +346,7 @@ export default {
:show-friendly-text="showFilteredSearchFriendlyText"
terms-as-tokens
class="gl-flex-grow-1 gl-border-t-none row-content-block"
- data-qa-selector="issuable_search_container"
+ data-testid="issuable-search-container"
@checked-input="handleAllIssuablesCheckedInput"
@onFilter="$emit('filter', $event)"
@onSort="$emit('sort', $event)"
@@ -377,7 +379,7 @@ export default {
v-for="issuable in issuables"
:key="issuableId(issuable)"
:class="{ 'gl-cursor-grab': isManualOrdering }"
- data-qa-selector="issuable_container"
+ data-testid="issuable-container"
:data-qa-issuable-title="issuable.title"
:has-scoped-labels-feature="hasScopedLabelsFeature"
:issuable-symbol="issuableSymbol"
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
index 0691bc02b5c..ab71842ae13 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
@@ -56,7 +56,7 @@ export default {
@click="$emit('click', tab.name)"
>
<template #title>
- <span :title="tab.titleTooltip" :data-qa-selector="`${tab.name}_issuables_tab`">
+ <span :title="tab.titleTooltip" :data-testid="`${tab.name}-issuables-tab`">
{{ tab.title }}
</span>
<gl-badge
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
index ce1851ab873..01389cd90a9 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue
@@ -34,7 +34,7 @@ export default {
<div
class="description"
:class="{ 'js-task-list-container': canEdit && enableTaskList }"
- data-qa-selector="description_content"
+ data-testid="description-content"
>
<div ref="gfmContainer" v-safe-html="issuable.descriptionHtml" class="md"></div>
<textarea
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 29aef89a991..c4b92454ac0 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
@@ -162,8 +162,8 @@ export default {
<template>
<div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row">
- <div class="detail-page-header-body gl-flex-wrap">
- <gl-badge class="gl-mr-2" :variant="badgeVariant">
+ <div class="detail-page-header-body gl-flex-wrap gl-gap-2">
+ <gl-badge :variant="badgeVariant" data-testid="issue-state-badge">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block" :class="{ 'gl-ml-2': statusIcon }">
<slot name="status-badge">{{ badgeText }}</slot>
@@ -193,18 +193,18 @@ export default {
<work-item-type-icon
v-if="shouldShowWorkItemTypeIcon"
show-text
- :work-item-type="issuableType.toUpperCase()"
+ :work-item-type="issuableType"
/>
<gl-sprintf :message="createdMessage">
<template #timeAgo>
- <time-ago-tooltip class="gl-mx-2" :time="createdAt" />
+ <time-ago-tooltip :time="createdAt" />
</template>
<template #email>
{{ serviceDeskReplyTo }}
</template>
<template #author>
<gl-link
- class="gl-font-weight-bold gl-mx-2 js-user-link"
+ class="gl-font-weight-bold js-user-link"
:href="author.webUrl"
:data-user-id="authorId"
>
@@ -225,7 +225,6 @@ export default {
<gl-icon
v-if="isFirstContribution"
v-gl-tooltip
- class="gl-mr-2"
name="first-contribution"
:title="__('1st contribution!')"
:aria-label="__('1st contribution!')"
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 2bc57ecba55..3878c16c8d0 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
@@ -92,6 +92,11 @@ export default {
required: false,
default: false,
},
+ workspaceType: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
handleKeydownTitle(e, issuableMeta) {
@@ -105,7 +110,7 @@ export default {
</script>
<template>
- <div class="issuable-show-container" data-qa-selector="issuable_show_container">
+ <div class="issuable-show-container" data-testid="issuable-show-container">
<issuable-header
:issuable-state="issuable.state"
:status-icon="statusIcon"
@@ -116,6 +121,7 @@ export default {
:author="issuable.author"
:task-completion-status="taskCompletionStatus"
:issuable-type="issuable.type"
+ :workspace-type="workspaceType"
:show-work-item-type-icon="showWorkItemTypeIcon"
>
<template #status-badge>
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 841d92fd63d..da71adc8abd 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
@@ -60,8 +60,7 @@ export default {
v-safe-html="issuable.titleHtml || issuable.title"
class="title gl-font-size-h-display"
dir="auto"
- data-qa-selector="title_content"
- data-testid="title"
+ data-testid="issuable-title"
></h1>
<gl-button
v-if="enableEdit"
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 4503ba6e561..f54c4c52743 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -88,6 +88,10 @@ export default {
showSuperSidebarToggle() {
return gon.use_new_navigation && sidebarState.isCollapsed;
},
+
+ topBarClasses() {
+ return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
+ },
},
created() {
@@ -120,15 +124,17 @@ export default {
<template>
<div>
- <div
- class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
- >
- <super-sidebar-toggle
- v-if="showSuperSidebarToggle"
- class="gl-mr-2"
- :class="$options.JS_TOGGLE_EXPAND_CLASS"
- />
- <gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
+ <div :class="topBarClasses" data-testid="top-bar">
+ <div
+ class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
+ >
+ <super-sidebar-toggle
+ v-if="showSuperSidebarToggle"
+ class="gl-mr-2"
+ :class="$options.JS_TOGGLE_EXPAND_CLASS"
+ />
+ <gl-breadcrumb :items="breadcrumbs" data-testid="breadcrumb-links" />
+ </div>
</div>
<template v-if="activePanel">
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
index 28618cb96a3..61bca18b050 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
@@ -1,15 +1,11 @@
<script>
-import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
name: 'SecurityReportDownloadDropdown',
components: {
- GlDropdown,
- GlDropdownItem,
- },
- directives: {
- GlTooltip,
+ GlDisclosureDropdown,
},
props: {
artifacts: {
@@ -26,19 +22,23 @@ export default {
required: false,
default: '',
},
- title: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
showDropdown() {
return this.loading || this.artifacts.length > 0;
},
+ items() {
+ return this.artifacts.map(({ name, path }) => ({
+ text: this.artifactText(name),
+ href: path,
+ extraAttrs: {
+ download: '',
+ },
+ }));
+ },
},
methods: {
- artifactText({ name }) {
+ artifactText(name) {
return sprintf(s__('SecurityReports|Download %{artifactName}'), {
artifactName: name,
});
@@ -48,23 +48,13 @@ export default {
</script>
<template>
- <gl-dropdown
+ <gl-disclosure-dropdown
v-if="showDropdown"
- v-gl-tooltip
- :text="text"
- :title="title"
+ :items="items"
+ :toggle-text="text"
:loading="loading"
icon="download"
size="small"
- right
- >
- <gl-dropdown-item
- v-for="artifact in artifacts"
- :key="artifact.path"
- :href="artifact.path"
- download
- >
- {{ artifactText(artifact) }}
- </gl-dropdown-item>
- </gl-dropdown>
+ placement="right"
+ />
</template>
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index bc3741a3880..07b16a13e68 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -20,7 +20,8 @@ export const mapComputed = (list, defaultUpdateFn, root) => {
get() {
if (getter) {
return this.$store.getters[getter];
- } else if (root) {
+ }
+ if (root) {
if (typeof root === 'function') {
return root(this.$store.state)[key];
}
diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js
index 1c6e632135d..ef82142289d 100644
--- a/app/assets/javascripts/webpack.js
+++ b/app/assets/javascripts/webpack.js
@@ -7,6 +7,7 @@
* e.g. the `window` scope, because it needs to be executed in the scope of webpack.
*/
-if (gon && gon.webpack_public_path) {
+// eslint-disable-next-line camelcase
+if (gon && gon.webpack_public_path && typeof __webpack_public_path__ !== 'undefined') {
__webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line camelcase
}
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
index 1ead16c944b..425679c1400 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_activity_sort_filter.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Tracking from '~/tracking';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
LocalStorageSync,
},
mixins: [Tracking.mixin()],
@@ -25,7 +24,7 @@ export default {
type: String,
required: true,
},
- filterOptions: {
+ items: {
type: Array,
required: true,
},
@@ -63,8 +62,7 @@ export default {
},
selectedSortOption() {
return (
- this.filterOptions.find(({ key }) => this.sortFilterProp === key) ||
- this.defaultSortFilterProp
+ this.items.find(({ key }) => this.sortFilterProp === key) || this.defaultSortFilterProp
);
},
},
@@ -94,23 +92,14 @@ export default {
as-string
@input="setDiscussionFilterOption"
/>
- <gl-dropdown
- class="gl-xs-w-full"
- size="small"
- :text="getDropdownSelectedText"
+ <gl-collapsible-listbox
+ :toggle-text="getDropdownSelectedText"
:disabled="loading"
- right
- >
- <gl-dropdown-item
- v-for="{ text, key, testid } in filterOptions"
- :key="text"
- :data-testid="testid"
- is-check-item
- :is-checked="isSortDropdownItemActive(key)"
- @click="fetchFilteredDiscussions(key)"
- >
- {{ text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ :items="items"
+ :selected="sortFilterProp"
+ placement="right"
+ size="small"
+ @select="fetchFilteredDiscussions"
+ />
</div>
</template>
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 66ad3d50287..57faed61280 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
@@ -74,6 +74,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -263,6 +268,7 @@ export default {
:work-item-id="workItemId"
:autofocus="autofocus"
:comment-button-text="commentButtonText"
+ :is-work-item-confidential="isWorkItemConfidential"
@submitForm="updateWorkItem"
@cancelEditing="cancelEditing"
@error="$emit('error', $event)"
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 b143c529014..a79169bde1e 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
@@ -3,11 +3,13 @@ import { GlButton, GlFormCheckbox, GlIcon, GlTooltipDirective } from '@gitlab/ui
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import { STATE_OPEN, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { STATE_OPEN, TRACKING_CATEGORY_SHOW, TASK_TYPE_NAME } from '~/work_items/constants';
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 CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
export default {
i18n: {
@@ -22,6 +24,7 @@ export default {
markdownDocsPath: helpPagePath('user/markdown'),
},
components: {
+ CommentFieldLayout,
GlButton,
MarkdownEditor,
GlFormCheckbox,
@@ -89,6 +92,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -119,6 +127,23 @@ export default {
commentButtonTextComputed() {
return this.isNoteInternal ? this.$options.i18n.addInternalNote : this.commentButtonText;
},
+ workItemDocPath() {
+ return this.workItemType === TASK_TYPE_NAME ? 'user/tasks.html' : 'user/okrs.html';
+ },
+ workItemDocAnchor() {
+ return this.workItemType === TASK_TYPE_NAME ? 'confidential-tasks' : 'confidential-okrs';
+ },
+ getWorkItemData() {
+ return {
+ confidential: this.isWorkItemConfidential,
+ confidential_issues_docs_path: helpPagePath(this.workItemDocPath, {
+ anchor: this.workItemDocAnchor,
+ }),
+ };
+ },
+ workItemTypeKey() {
+ return capitalizeFirstCharacter(this.workItemType).replace(' ', '');
+ },
},
methods: {
setCommentText(newText) {
@@ -158,66 +183,73 @@ export default {
<template>
<div class="timeline-discussion-body gl-overflow-visible!">
<div class="note-body gl-p-0! gl-overflow-visible!">
- <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
- <markdown-editor
- :value="commentText"
- :render-markdown-path="markdownPreviewPath"
- :markdown-docs-path="$options.constantOptions.markdownDocsPath"
- :autocomplete-data-sources="autocompleteDataSources"
- :form-field-props="formFieldProps"
- :add-spacing-classes="false"
- data-testid="work-item-add-comment"
- class="gl-mb-5"
- use-bottom-toolbar
- supports-quick-actions
- :autofocus="autofocus"
- @input="setCommentText"
- @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
- @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
- @keydown.esc.stop="cancelEditing"
- />
- <gl-form-checkbox
- v-if="isNewDiscussion"
- v-model="isNoteInternal"
- class="gl-mb-2"
- data-testid="internal-note-checkbox"
+ <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1 new-note">
+ <comment-field-layout
+ :with-alert-container="isWorkItemConfidential"
+ :noteable-data="getWorkItemData"
+ :noteable-type="workItemTypeKey"
>
- {{ $options.i18n.internal }}
- <gl-icon
- v-gl-tooltip:tooltipcontainer.bottom
- name="question-o"
- :size="16"
- :title="$options.i18n.internalVisibility"
- class="gl-text-blue-500"
+ <markdown-editor
+ :value="commentText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.constantOptions.markdownDocsPath"
+ :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"
+ @input="setCommentText"
+ @keydown.meta.enter="$emit('submitForm', { commentText, isNoteInternal })"
+ @keydown.ctrl.enter="$emit('submitForm', { commentText, isNoteInternal })"
+ @keydown.esc.stop="cancelEditing"
+ />
+ </comment-field-layout>
+ <div class="note-form-actions">
+ <gl-form-checkbox
+ v-if="isNewDiscussion"
+ v-model="isNoteInternal"
+ class="gl-mb-2"
+ data-testid="internal-note-checkbox"
+ >
+ {{ $options.i18n.internal }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question-o"
+ :size="16"
+ :title="$options.i18n.internalVisibility"
+ class="gl-text-blue-500"
+ />
+ </gl-form-checkbox>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ data-testid="confirm-button"
+ :disabled="!commentText.length"
+ :loading="isSubmitting"
+ @click="$emit('submitForm', { commentText, isNoteInternal })"
+ >{{ commentButtonTextComputed }}
+ </gl-button>
+ <work-item-state-toggle-button
+ v-if="isNewDiscussion"
+ class="gl-ml-3"
+ :work-item-id="workItemId"
+ :work-item-state="workItemState"
+ :work-item-type="workItemType"
+ can-update
+ @error="$emit('error', $event)"
/>
- </gl-form-checkbox>
- <gl-button
- category="primary"
- variant="confirm"
- data-testid="confirm-button"
- :disabled="!commentText.length"
- :loading="isSubmitting"
- @click="$emit('submitForm', { commentText, isNoteInternal })"
- >{{ commentButtonTextComputed }}
- </gl-button>
- <work-item-state-toggle-button
- v-if="isNewDiscussion"
- class="gl-ml-3"
- :work-item-id="workItemId"
- :work-item-state="workItemState"
- :work-item-type="workItemType"
- can-update
- @error="$emit('error', $event)"
- />
- <gl-button
- v-else
- data-testid="cancel-button"
- category="primary"
- class="gl-ml-3"
- :loading="updateInProgress"
- @click="cancelEditing"
- >{{ $options.i18n.cancelButtonText }}
- </gl-button>
+ <gl-button
+ v-else
+ data-testid="cancel-button"
+ category="primary"
+ class="gl-ml-3"
+ :loading="updateInProgress"
+ @click="cancelEditing"
+ >{{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ </div>
</form>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
index f030363664f..fd8842aa01a 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue
@@ -65,6 +65,11 @@ export default {
required: false,
default: false,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -235,6 +240,7 @@ export default {
:autocomplete-data-sources="autocompleteDataSources"
:markdown-preview-path="markdownPreviewPath"
:is-internal-thread="note.internal"
+ :is-work-item-confidential="isWorkItemConfidential"
@startReplying="showReplyForm"
@cancelEditing="hideReplyForm"
@replied="onReplied"
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 92560f2da9e..b5e3ea68725 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
@@ -163,6 +163,9 @@ export default {
projectName() {
return this.workItem?.project?.name;
},
+ isWorkItemConfidential() {
+ return this.workItem?.confidential;
+ },
},
apollo: {
workItem: {
@@ -314,6 +317,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:work-item-id="workItemId"
:autofocus="isEditing"
+ :is-work-item-confidential="isWorkItemConfidential"
class="gl-pl-3 gl-mt-3"
@cancelEditing="isEditing = false"
@submitForm="updateNote"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
index 0c1419e983f..1578c78ac4f 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
@@ -64,7 +64,7 @@ export default {
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="discussionFilter"
- :filter-options="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
+ :items="$options.WORK_ITEM_ACTIVITY_FILTER_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_FILTER_KEY"
:default-sort-filter-prop="$options.WORK_ITEM_NOTES_FILTER_ALL_NOTES"
tracking-action="work_item_notes_filter_changed"
@@ -77,7 +77,7 @@ export default {
:work-item-type="workItemType"
:loading="disableActivityFilterSort"
:sort-filter-prop="sortOrder"
- :filter-options="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
+ :items="$options.WORK_ITEM_ACTIVITY_SORT_OPTIONS"
:storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
:default-sort-filter-prop="$options.ASC"
tracking-action="work_item_notes_sort_order_changed"
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 0a38dcb77f6..f50cfac90f7 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -43,15 +43,6 @@ export default {
type: Boolean,
required: true,
},
- parentWorkItemId: {
- type: String,
- required: true,
- },
- workItemType: {
- type: String,
- required: false,
- default: '',
- },
childPath: {
type: String,
required: true,
@@ -158,7 +149,7 @@ export default {
</span>
<gl-link
:href="childPath"
- class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ class="gl-text-truncate gl-font-weight-semibold"
data-testid="item-title"
@click="$emit('click', $event)"
@mouseover="$emit('mouseover')"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
index ddeac2b92ae..38d8d239a7e 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
@@ -42,7 +42,8 @@ export default {
assigneesContainerClass() {
if (this.assignees.length === 2) {
return 'fixed-width-avatars-2';
- } else if (this.assignees.length > 2) {
+ }
+ if (this.assignees.length > 2) {
return 'fixed-width-avatars-3';
}
return '';
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
index 53e8eedf060..12b7bade31d 100644
--- 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
@@ -1,29 +1,28 @@
<script>
-import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
export default {
components: {
- GlDropdownItem,
- GlDropdown,
- GlIcon,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdown,
},
};
</script>
<template>
<div class="gl-ml-5">
- <gl-dropdown
+ <gl-disclosure-dropdown
category="tertiary"
toggle-class="btn-icon btn-sm"
- :right="true"
+ icon="ellipsis_v"
data-testid="work_items_links_menu"
+ :aria-label="__(`More actions`)"
+ text-sr-only
+ no-caret
>
- <template #button-content>
- <gl-icon name="ellipsis_v" :size="14" />
- </template>
- <gl-dropdown-item @click="$emit('removeChild')">
- {{ s__('WorkItem|Remove') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <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
new file mode 100644
index 00000000000..7b38e838033
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -0,0 +1,145 @@
+<script>
+import { GlTokenSelector } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
+import {
+ WORK_ITEMS_TYPE_MAP,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ sprintfWorkItem,
+} from '../../constants';
+
+export default {
+ components: {
+ GlTokenSelector,
+ },
+ props: {
+ value: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ childrenType: {
+ type: String,
+ required: false,
+ default: WORK_ITEM_TYPE_ENUM_TASK,
+ },
+ childrenIds: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ areWorkItemsToAddValid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ apollo: {
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.search?.title || this.search,
+ types: [this.childrenType],
+ in: this.search ? 'TITLE' : undefined,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.parentWorkItemId !== wi.id,
+ );
+ },
+ },
+ },
+ data() {
+ return {
+ availableWorkItems: [],
+ search: '',
+ searchStarted: false,
+ };
+ },
+ computed: {
+ workItemsToAdd: {
+ get() {
+ return this.value;
+ },
+ set(workItemsToAdd) {
+ this.$emit('input', workItemsToAdd);
+ },
+ },
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ addInputPlaceholder() {
+ return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
+ },
+ childrenTypeName() {
+ return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
+ },
+ tokenSelectorContainerClass() {
+ return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ methods: {
+ getIdFromGraphQLId,
+ setSearchKey(value) {
+ this.search = value;
+ },
+ handleFocus() {
+ this.searchStarted = true;
+ },
+ handleMouseOver() {
+ this.timeout = setTimeout(() => {
+ this.searchStarted = true;
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ handleMouseOut() {
+ clearTimeout(this.timeout);
+ },
+ },
+};
+</script>
+<template>
+ <gl-token-selector
+ 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"
+ >
+ <template #token-content="{ token }">
+ {{ token.title }}
+ </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <div class="gl-display-flex">
+ <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
+ <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
+ </div>
+ </template>
+ </gl-token-selector>
+</template>
diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue
index f343f787358..27de858fe4e 100644
--- a/app/assets/javascripts/work_items/components/widget_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue
@@ -14,6 +14,11 @@ export default {
required: false,
default: '',
},
+ widgetName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -30,6 +35,12 @@ export default {
isOpenString() {
return this.isOpen ? 'true' : 'false';
},
+ anchorLink() {
+ return `#${this.widgetName}`;
+ },
+ anchorLinkId() {
+ return `user-content-${this.widgetName}-links`;
+ },
},
methods: {
hide() {
@@ -46,14 +57,14 @@ export default {
</script>
<template>
- <div id="tasks" class="gl-new-card" :aria-expanded="isOpenString">
+ <div :id="widgetName" class="gl-new-card" :aria-expanded="isOpenString">
<div class="gl-new-card-header">
<div class="gl-new-card-title-wrapper">
<h3 class="gl-new-card-title">
<gl-link
- id="user-content-tasks-links"
- class="anchor position-absolute gl-text-decoration-none"
- href="#tasks"
+ :id="anchorLinkId"
+ class="gl-text-decoration-none"
+ :href="anchorLink"
aria-hidden="true"
/>
<slot name="header"></slot>
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 e8fe64c932b..18aa4d55086 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -1,8 +1,7 @@
<script>
import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownForm,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlDropdownDivider,
GlModal,
GlModalDirective,
@@ -53,9 +52,8 @@ export default {
emailAddressCopied: __('Email address copied'),
},
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownForm,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
GlDropdownDivider,
GlModal,
GlToggle,
@@ -180,13 +178,16 @@ export default {
navigator.clipboard.writeText(text);
}
toast(message);
+ this.closeDropdown();
},
handleToggleWorkItemConfidentiality() {
this.track('click_toggle_work_item_confidentiality');
this.$emit('toggleWorkItemConfidentiality', !this.isConfidential);
+ this.closeDropdown();
},
handleDelete() {
this.$refs.modal.show();
+ this.closeDropdown();
},
handleDeleteWorkItem() {
this.track('click_delete_work_item');
@@ -275,6 +276,9 @@ export default {
throwConvertError() {
this.$emit('error', this.i18n.convertError);
},
+ closeDropdown() {
+ this.$refs.workItemsMoreActions.close();
+ },
async promoteToObjective() {
try {
const {
@@ -300,6 +304,8 @@ export default {
} catch (error) {
this.throwConvertError();
Sentry.captureException(error);
+ } finally {
+ this.closeDropdown();
}
},
},
@@ -308,77 +314,87 @@ export default {
<template>
<div>
- <gl-dropdown
+ <gl-disclosure-dropdown
+ ref="workItemsMoreActions"
icon="ellipsis_v"
data-testid="work-item-actions-dropdown"
text-sr-only
:text="__('More actions')"
category="tertiary"
+ :auto-close="false"
no-caret
right
>
<template v-if="$options.isLoggedIn">
- <gl-dropdown-form
- class="work-item-notifications-form"
+ <gl-disclosure-dropdown-item
+ class="gl-display-flex gl-justify-content-end gl-w-full"
:data-testid="$options.notificationsToggleFormTestId"
>
- <div class="gl-px-5 gl-pb-2 gl-pt-1">
+ <template #list-item>
<gl-toggle
:value="subscribedToNotifications"
:label="$options.i18n.notifications"
:data-testid="$options.notificationsToggleTestId"
+ class="work-item-notification-toggle"
label-position="left"
label-id="notifications-toggle"
@change="toggleNotifications($event)"
/>
- </div>
- </gl-dropdown-form>
+ </template>
+ </gl-disclosure-dropdown-item>
<gl-dropdown-divider />
</template>
- <gl-dropdown-item
+
+ <gl-disclosure-dropdown-item
v-if="canPromoteToObjective"
:data-testid="$options.promoteActionTestId"
- @click="promoteToObjective"
+ @action="promoteToObjective"
>
- {{ __('Promote to objective') }}
- </gl-dropdown-item>
+ <template #list-item>{{ __('Promote to objective') }}</template>
+ </gl-disclosure-dropdown-item>
<template v-if="canUpdate && !isParentConfidential">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
:data-testid="$options.confidentialityTestId"
- @click="handleToggleWorkItemConfidentiality"
- >{{
+ @action="handleToggleWorkItemConfidentiality"
+ ><template #list-item>{{
isConfidential
? $options.i18n.disableTaskConfidentiality
: $options.i18n.enableTaskConfidentiality
- }}</gl-dropdown-item
+ }}</template></gl-disclosure-dropdown-item
>
</template>
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
ref="workItemReference"
:data-testid="$options.copyReferenceTestId"
:data-clipboard-text="workItemReference"
- @click="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
- >{{ $options.i18n.copyReference }}</gl-dropdown-item
+ @action="copyToClipboard(workItemReference, $options.i18n.referenceCopied)"
+ ><template #list-item>{{
+ $options.i18n.copyReference
+ }}</template></gl-disclosure-dropdown-item
>
<template v-if="$options.isLoggedIn && workItemCreateNoteEmail">
- <gl-dropdown-item
+ <gl-disclosure-dropdown-item
ref="workItemCreateNoteEmail"
:data-testid="$options.copyCreateNoteEmailTestId"
:data-clipboard-text="workItemCreateNoteEmail"
- @click="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
- >{{ i18n.copyCreateNoteEmail }}</gl-dropdown-item
+ @action="copyToClipboard(workItemCreateNoteEmail, $options.i18n.emailAddressCopied)"
+ ><template #list-item>{{
+ i18n.copyCreateNoteEmail
+ }}</template></gl-disclosure-dropdown-item
>
- <gl-dropdown-divider v-if="canDelete" />
</template>
- <gl-dropdown-item
+ <gl-dropdown-divider v-if="canDelete" />
+ <gl-disclosure-dropdown-item
v-if="canDelete"
:data-testid="$options.deleteActionTestId"
variant="danger"
- @click="handleDelete"
+ @action="handleDelete"
>
- {{ i18n.deleteWorkItem }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <template #list-item
+ ><span class="text-danger">{{ i18n.deleteWorkItem }}</span></template
+ >
+ </gl-disclosure-dropdown-item>
+ </gl-disclosure-dropdown>
<gl-modal
ref="modal"
modal-id="work-item-confirm-delete"
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4b4aa7f96ca..f9527884adc 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -388,7 +388,7 @@ export default {
:display-text="__('Invite members')"
trigger-element="side-nav"
icon="plus"
- trigger-source="work-item-assignees-dropdown"
+ trigger-source="work_item_assignees_dropdown"
classes="gl-display-block gl-text-body! gl-hover-text-decoration-none gl-pb-2"
/>
</gl-dropdown-item>
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 f93ea4a0753..14e55134048 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
@@ -84,13 +84,13 @@ export default {
<gl-loading-icon v-if="updateInProgress" :inline="true" class="gl-mr-3" />
<confidentiality-badge
v-if="isWorkItemConfidential"
- class="gl-vertical-align-middle gl-display-inline-flex!"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-vertical-align-middle gl-display-inline-flex! gl-mr-2"
:issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ hide-text-in-small-screens
/>
<work-item-type-icon
- class="gl-vertical-align-middle gl-mr-0!"
+ class="gl-vertical-align-middle"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType"
show-text
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 d826ef9cbe7..edecd7addcc 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -31,6 +31,7 @@ import {
WORK_ITEM_TYPE_VALUE_ISSUE,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WIDGET_TYPE_NOTES,
+ WIDGET_TYPE_LINKED_ITEMS,
} from '../constants';
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
@@ -50,6 +51,7 @@ 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';
export default {
i18n,
@@ -79,6 +81,7 @@ export default {
AbuseCategorySelector,
GlIntersectionObserver,
ConfidentialityBadge,
+ WorkItemRelationships,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'reportAbusePath'],
@@ -259,6 +262,15 @@ export default {
showIntersectionObserver() {
return !this.isModal && this.workItemsMvc2Enabled;
},
+ hasLinkedWorkItems() {
+ return this.glFeatures.linkedWorkItems;
+ },
+ workItemLinkedItems() {
+ return this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS);
+ },
+ showWorkItemLinkedItems() {
+ return this.hasLinkedWorkItems && this.workItemLinkedItems;
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -515,9 +527,9 @@ export default {
<gl-loading-icon v-if="updateInProgress" class="gl-mr-3" />
<confidentiality-badge
v-if="workItem.confidential"
- data-testid="confidential"
- :workspace-type="$options.WORKSPACE_PROJECT"
+ class="gl-mr-3"
:issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
/>
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
@@ -591,6 +603,12 @@ export default {
@show-modal="openInModal"
@addChild="$emit('addChild')"
/>
+ <work-item-relationships
+ v-if="showWorkItemLinkedItems"
+ :work-item-iid="workItemIid"
+ :work-item-full-path="workItem.project.fullPath"
+ @showModal="openInModal"
+ />
<work-item-notes
v-if="workItemNotes"
:work-item-id="workItem.id"
@@ -600,6 +618,7 @@ export default {
:assignees="workItemAssignees && workItemAssignees.assignees.nodes"
:can-set-work-item-metadata="canAssignUnassignUser"
:report-abuse-path="reportAbusePath"
+ :is-work-item-confidential="workItem.confidential"
class="gl-pt-5"
@error="updateError = $event"
@has-notes="updateHasNotes"
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 bf427feaa35..9d9414b5399 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
@@ -56,14 +56,14 @@ export default {
return isLoggedIn() && this.canUpdate;
},
treeRootWrapper() {
- return this.canReorder ? Draggable : 'div';
+ return this.canReorder ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
...defaultSortableOptions,
fallbackOnBody: false,
group: 'sortable-container',
- tag: 'div',
+ tag: 'ul',
'ghost-class': 'tree-item-drag-active',
'data-parent-id': this.workItemId,
value: this.children,
@@ -248,6 +248,7 @@ export default {
<component
:is="treeRootWrapper"
v-bind="treeRootOptions"
+ class="content-list"
:class="{ 'gl-cursor-grab sortable-container': canReorder }"
@end="handleDragOnEnd"
>
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 a9b0c2b98bf..679287338c8 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -13,6 +13,7 @@ import {
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
+import { workItemPath } from '../../utils';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
@@ -90,7 +91,7 @@ export default {
return this.isItemOpen ? __('Created') : __('Closed');
},
childPath() {
- return `${gon?.relative_url_root || ''}/${this.fullPath}/-/work_items/${this.childItem.iid}`;
+ return workItemPath(this.fullPath, this.childItem.iid);
},
chevronType() {
return this.isExpanded ? 'chevron-down' : 'chevron-right';
@@ -212,7 +213,7 @@ export default {
</script>
<template>
- <div class="tree-item">
+ <li class="tree-item">
<div
class="gl-display-flex gl-align-items-flex-start"
:class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
@@ -249,5 +250,5 @@ export default {
@removeChild="removeChild"
@click="$emit('click', $event)"
/>
- </div>
+ </li>
</template>
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 a0ff693e156..eb836007e75 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
@@ -103,6 +103,7 @@ export default {
isReportDrawerOpen: false,
reportedUserId: 0,
reportedUrl: '',
+ widgetName: 'tasks',
};
},
computed: {
@@ -166,7 +167,6 @@ export default {
this.updateWorkItemIdUrlQuery(child);
},
async closeModal() {
- this.activeChild = {};
this.updateWorkItemIdUrlQuery();
},
handleWorkItemDeleted(child) {
@@ -206,6 +206,7 @@ export default {
<widget-wrapper
ref="wrapper"
:error="error"
+ :widget-name="widgetName"
data-testid="work-item-links"
@dismissAlert="error = undefined"
>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index 4960189fb48..55440e1603c 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -3,19 +3,15 @@ import {
GlAlert,
GlFormGroup,
GlForm,
- GlTokenSelector,
GlButton,
GlFormInput,
GlFormCheckbox,
GlTooltip,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
+import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
-import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
import {
@@ -23,7 +19,6 @@ import {
WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_TASK,
I18N_WORK_ITEM_CREATE_BUTTON_LABEL,
- I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_ADD_BUTTON_LABEL,
I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
@@ -35,12 +30,12 @@ export default {
components: {
GlAlert,
GlForm,
- GlTokenSelector,
GlButton,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlTooltip,
+ WorkItemTokenInput,
},
inject: ['fullPath', 'hasIterationsFeature'],
props: {
@@ -101,35 +96,14 @@ export default {
return data.workspace?.workItemTypes?.nodes;
},
},
- availableWorkItems: {
- query: projectWorkItemsQuery,
- variables() {
- return {
- fullPath: this.fullPath,
- searchTerm: this.search?.title || this.search,
- types: [this.childrenType],
- in: this.search ? 'TITLE' : undefined,
- };
- },
- skip() {
- return !this.searchStarted;
- },
- update(data) {
- return data.workspace.workItems.nodes.filter(
- (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id,
- );
- },
- },
},
data() {
return {
workItemTypes: [],
- availableWorkItems: [],
- search: '',
- searchStarted: false,
+ workItemsToAdd: [],
error: null,
+ search: '',
childToCreateTitle: null,
- workItemsToAdd: [],
confidential: this.parentConfidential,
};
},
@@ -177,7 +151,8 @@ export default {
addOrCreateButtonLabel() {
if (this.isCreateForm) {
return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName);
- } else if (this.workItemsToAdd.length > 1) {
+ }
+ if (this.workItemsToAdd.length > 1) {
return sprintfWorkItem(I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, this.childrenTypeName);
}
return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
@@ -216,15 +191,6 @@ export default {
}
return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
- isLoading() {
- return this.$apollo.queries.availableWorkItems.loading;
- },
- addInputPlaceholder() {
- return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
- },
- tokenSelectorContainerClass() {
- return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
- },
invalidWorkItemsToAdd() {
return this.parentConfidential
? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
@@ -249,11 +215,7 @@ export default {
);
},
},
- created() {
- this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
methods: {
- getIdFromGraphQLId,
getConfidentialityTooltipTarget() {
// We want tooltip to be anchored to `input` within checkbox component
// but `$el.querySelector('input')` doesn't work. 🤷‍♂️
@@ -317,20 +279,6 @@ export default {
this.childToCreateTitle = null;
});
},
- setSearchKey(value) {
- this.search = value;
- },
- handleFocus() {
- this.searchStarted = true;
- },
- handleMouseOver() {
- this.timeout = setTimeout(() => {
- this.searchStarted = true;
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
- },
- handleMouseOut() {
- clearTimeout(this.timeout);
- },
},
i18n: {
inputLabel: __('Title'),
@@ -385,30 +333,16 @@ export default {
>{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
<div class="gl-mb-4">
- <gl-token-selector
+ <work-item-token-input
v-if="!isCreateForm"
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"
- >
- <template #token-content="{ token }">
- {{ token.title }}
- </template>
- <template #dropdown-item-content="{ dropdownItem }">
- <div class="gl-display-flex">
- <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
- <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
- </div>
- </template>
- </gl-token-selector>
+ :is-create-form="isCreateForm"
+ :parent-work-item-id="issuableGid"
+ :children-type="childrenType"
+ :children-ids="childrenIds"
+ :are-work-items-to-add-valid="areWorkItemsToAddValid"
+ :full-path="fullPath"
+ />
<div
v-if="showWorkItemsToAddInvalidMessage"
class="gl-text-red-500"
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 246eac82c78..bc3f5201fb8 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
@@ -64,6 +64,7 @@ export default {
isShownAddForm: false,
formType: null,
childType: null,
+ widgetName: 'tasks',
};
},
computed: {
@@ -101,6 +102,7 @@ export default {
<template>
<widget-wrapper
ref="wrapper"
+ :widget-name="widgetName"
:error="error"
data-testid="work-item-tree"
@dismissAlert="error = undefined"
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 8fc460294e6..256f8ed53d1 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -79,6 +79,11 @@ export default {
type: String,
required: true,
},
+ isWorkItemConfidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -124,6 +129,7 @@ export default {
isNewDiscussion: true,
markdownPreviewPath: this.markdownPreviewPath,
autocompleteDataSources: this.autocompleteDataSources,
+ isWorkItemConfidential: this.isWorkItemConfidential,
};
},
notesArray() {
@@ -366,6 +372,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:assignees="assignees"
:can-set-work-item-metadata="canSetWorkItemMetadata"
+ :is-work-item-confidential="isWorkItemConfidential"
@deleteNote="showDeleteNoteModal($event, discussion)"
@reportAbuse="reportAbuse(true, $event)"
@error="$emit('error', $event)"
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
new file mode 100644
index 00000000000..cbe830f9565
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationship_list.vue
@@ -0,0 +1,61 @@
+<script>
+import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
+import { workItemPath } from '../../utils';
+
+export default {
+ components: {
+ WorkItemLinkChildContents,
+ },
+ props: {
+ linkedItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ heading: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ linkedItemPath(fullPath, id) {
+ return workItemPath(fullPath, id);
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h4
+ v-if="heading"
+ data-testid="work-items-list-heading"
+ class="gl-font-sm gl-font-weight-semibold gl-text-gray-700 gl-mx-2 gl-mt-3 gl-mb-2"
+ >
+ {{ heading }}
+ </h4>
+ <div class="work-items-list-body">
+ <ul ref="list" class="work-items-list content-list">
+ <li
+ v-for="linkedItem in linkedItems"
+ :key="linkedItem.workItem.id"
+ class="gl-pt-0! gl-pb-0! gl-border-b-0!"
+ >
+ <work-item-link-child-contents
+ :child-item="linkedItem.workItem"
+ :can-update="canUpdate"
+ :child-path="linkedItemPath(workItemFullPath, linkedItem.workItem.iid)"
+ @click="$emit('showModal', { event: $event, child: linkedItem.workItem })"
+ />
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
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
new file mode 100644
index 00000000000..4f6879e9605
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_relationships.vue
@@ -0,0 +1,185 @@
+<script>
+import { GlLoadingIcon, GlIcon, GlButton } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import { WIDGET_TYPE_LINKED_ITEMS, LINKED_CATEGORIES_MAP } from '../../constants';
+
+import WidgetWrapper from '../widget_wrapper.vue';
+import WorkItemRelationshipList from './work_item_relationship_list.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ GlButton,
+ WidgetWrapper,
+ WorkItemRelationshipList,
+ },
+ props: {
+ workItemIid: {
+ type: String,
+ required: true,
+ },
+ workItemFullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ workItem: {
+ query: workItemByIidQuery,
+ variables() {
+ return {
+ fullPath: this.workItemFullPath,
+ iid: this.workItemIid,
+ };
+ },
+ update(data) {
+ return data.workspace.workItems.nodes[0] ?? {};
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ skip() {
+ return !this.workItemIid;
+ },
+ error(e) {
+ this.error = e.message || this.$options.i18n.fetchError;
+ },
+ async result() {
+ // When work items are switched in a modal, the data props are not getting reset.
+ // Thus, duplicating the work items in the list.
+ // Here, the existing list are cleared before the new items are pushed.
+ this.linksRelatesTo = [];
+ this.linksIsBlockedBy = [];
+ this.linksBlocks = [];
+
+ this.linkedWorkItems.forEach((item) => {
+ if (item.linkType === LINKED_CATEGORIES_MAP.RELATES_TO) {
+ this.linksRelatesTo.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.IS_BLOCKED_BY) {
+ this.linksIsBlockedBy.push(item);
+ } else if (item.linkType === LINKED_CATEGORIES_MAP.BLOCKS) {
+ this.linksBlocks.push(item);
+ }
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ error: '',
+ linksRelatesTo: [],
+ linksIsBlockedBy: [],
+ linksBlocks: [],
+ widgetName: 'linkeditems',
+ };
+ },
+ computed: {
+ canUpdate() {
+ // This will be false untill we implement remove item mutation
+ return false;
+ },
+ isLoading() {
+ return this.$apollo.queries.workItem.loading;
+ },
+ linkedWorkItemsWidget() {
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LINKED_ITEMS);
+ },
+ linkedWorkItems() {
+ return this.linkedWorkItemsWidget?.linkedItems?.nodes || [];
+ },
+ linkedWorkItemsCount() {
+ return this.linkedWorkItems.length;
+ },
+ isEmptyRelatedWorkItems() {
+ return !this.error && this.linkedWorkItems.length === 0;
+ },
+ },
+ i18n: {
+ title: s__('WorkItem|Linked Items'),
+ fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
+ emptyStateMessage: s__(
+ "WorkItem|Link work items together to show that they're related or that one is blocking others.",
+ ),
+ addChildButtonLabel: s__('WorkItem|Add'),
+ relatedToTitle: s__('WorkItem|Related to'),
+ blockingTitle: s__('WorkItem|Blocking'),
+ blockedByTitle: s__('WorkItem|Blocked by'),
+ addLinkedWorkItemButtonLabel: s__('WorkItem|Add'),
+ },
+};
+</script>
+<template>
+ <widget-wrapper
+ :error="error"
+ class="work-item-relationships"
+ :widget-name="widgetName"
+ @dismissAlert="error = undefined"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h3 class="gl-new-card-title">
+ {{ $options.i18n.title }}
+ </h3>
+ <div v-if="linkedWorkItemsCount" class="gl-new-card-count">
+ <gl-icon name="link" class="gl-mr-2" />
+ <span data-testid="linked-items-count">{{ linkedWorkItemsCount }}</span>
+ </div>
+ </div>
+ </template>
+ <template #header-right>
+ <gl-button size="small" class="gl-ml-3">
+ <slot name="add-button-text">{{ $options.i18n.addLinkedWorkItemButtonLabel }}</slot>
+ </gl-button>
+ </template>
+ <template #body>
+ <div class="gl-new-card-content">
+ <gl-loading-icon v-if="isLoading" color="dark" class="gl-my-2" />
+ <template v-else>
+ <div v-if="isEmptyRelatedWorkItems" data-testid="links-empty">
+ <p class="gl-new-card-empty">
+ {{ $options.i18n.emptyStateMessage }}
+ </p>
+ </div>
+ <template v-else>
+ <work-item-relationship-list
+ v-if="linksBlocks.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksIsBlockedBy.length,
+ }"
+ :linked-items="linksBlocks"
+ :heading="$options.i18n.blockingTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksIsBlockedBy.length"
+ :class="{
+ 'gl-pb-3 gl-mb-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100':
+ linksRelatesTo.length,
+ }"
+ :linked-items="linksIsBlockedBy"
+ :heading="$options.i18n.blockedByTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ <work-item-relationship-list
+ v-if="linksRelatesTo.length"
+ :linked-items="linksRelatesTo"
+ :heading="$options.i18n.relatedToTitle"
+ :work-item-full-path="workItemFullPath"
+ :can-update="canUpdate"
+ @showModal="$emit('showModal', { event: $event.event, modalWorkItem: $event.child })"
+ />
+ </template>
+ </template>
+ </div>
+ </template>
+ </widget-wrapper>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_badge.vue b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
index 1d1bc7352b1..5c5b41b38e6 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_badge.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_badge.vue
@@ -1,11 +1,12 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { STATE_OPEN } from '../constants';
export default {
components: {
GlBadge,
+ GlIcon,
},
props: {
workItemState: {
@@ -31,11 +32,8 @@ export default {
</script>
<template>
- <gl-badge
- :icon="workItemStateIcon"
- :variant="workItemStateVariant"
- class="gl-mr-2 gl-vertical-align-middle"
- >
- {{ stateText }}
+ <gl-badge :variant="workItemStateVariant" class="gl-mr-2 gl-vertical-align-middle">
+ <gl-icon :name="workItemStateIcon" :size="16" />
+ <span class="gl-display-none gl-sm-display-block gl-ml-2">{{ stateText }}</span>
</gl-badge>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index f27ae5f4e6d..5426f3965b3 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -53,7 +53,7 @@ export default {
</script>
<template>
- <span class="gl-mr-2">
+ <span>
<gl-icon
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 57206550328..2b118247426 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -26,6 +26,7 @@ export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
export const WIDGET_TYPE_NOTES = 'NOTES';
export const WIDGET_TYPE_HEALTH_STATUS = 'HEALTH_STATUS';
+export const WIDGET_TYPE_LINKED_ITEMS = 'LINKED_ITEMS';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
@@ -35,6 +36,7 @@ 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_VALUE_EPIC = 'Epic';
export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
export const WORK_ITEM_TYPE_VALUE_ISSUE = 'Issue';
export const WORK_ITEM_TYPE_VALUE_TASK = 'Task';
@@ -57,6 +59,9 @@ export const i18n = {
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
'WorkItem|Something went wrong when fetching labels. Please try again.',
);
+export const I18N_WORK_ITEM_ERROR_FETCHING_TYPES = s__(
+ 'WorkItem|Something went wrong when fetching work item types. Please try again',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -208,24 +213,22 @@ export const WORK_ITEM_NOTES_FILTER_KEY = 'filter_key_work_item';
export const WORK_ITEM_ACTIVITY_FILTER_OPTIONS = [
{
- key: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
+ value: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
text: s__('WorkItem|All activity'),
},
{
- key: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
+ value: WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS,
text: s__('WorkItem|Comments only'),
- testid: 'comments-activity',
},
{
- key: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
+ value: WORK_ITEM_NOTES_FILTER_ONLY_HISTORY,
text: s__('WorkItem|History only'),
- testid: 'history-activity',
},
];
export const WORK_ITEM_ACTIVITY_SORT_OPTIONS = [
- { key: DESC, text: __('Newest first'), testid: 'newest-first' },
- { key: ASC, text: __('Oldest first') },
+ { value: DESC, text: __('Newest first') },
+ { value: ASC, text: __('Oldest first') },
];
export const TEST_ID_CONFIDENTIALITY_TOGGLE_ACTION = 'confidentiality-toggle-action';
@@ -241,10 +244,6 @@ export const TODO_DONE_ICON = 'todo-done';
export const TODO_DONE_STATE = 'done';
export const TODO_PENDING_STATE = 'pending';
-export const CURRENT_USER_TODOS_TYPENAME = 'WorkItemWidgetCurrentUserTodos';
-
-export const EMOJI_ACTION_ADD = 'ADD';
-export const EMOJI_ACTION_REMOVE = 'REMOVE';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
@@ -257,3 +256,9 @@ export const WORK_ITEM_TO_ISSUE_MAP = {
[WIDGET_TYPE_HEALTH_STATUS]: 'healthStatus',
[WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
};
+
+export const LINKED_CATEGORIES_MAP = {
+ RELATES_TO: 'relates_to',
+ IS_BLOCKED_BY: 'is_blocked_by',
+ BLOCKS: 'blocks',
+};
diff --git a/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql b/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql
new file mode 100644
index 00000000000..30757f57234
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/group_work_item_types.query.graphql
@@ -0,0 +1,11 @@
+query groupWorkItemTypes($fullPath: ID!) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItemTypes {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
index 383d003e78c..ffc9fe2f7f7 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql
@@ -100,4 +100,31 @@ fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetAwardEmoji {
type
}
+
+ ... on WorkItemWidgetLinkedItems {
+ type
+ linkedItems {
+ nodes {
+ linkId
+ linkType
+ workItem {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ...WorkItemMetadataWidgets
+ }
+ }
+ }
+ }
+ }
}
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 fe7cb719bbb..a853018a931 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,7 @@
<script>
import * as Sentry from '@sentry/browser';
+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';
import { __, s__ } from '~/locale';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
@@ -8,12 +10,11 @@ import { STATE_CLOSED } from '../../constants';
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
export default {
- i18n: {
- searchPlaceholder: __('Search or filter results...'),
- },
issuableListTabs,
components: {
IssuableList,
+ IssueCardStatistics,
+ IssueCardTimeInfo,
},
inject: ['fullPath'],
data() {
@@ -57,17 +58,33 @@ export default {
:current-tab="state"
:error="error"
:issuables="workItems"
+ :issuables-loading="$apollo.queries.workItems.loading"
namespace="work-items"
recent-searches-storage-key="issues"
- :search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
show-work-item-type-icon
:sort-options="sortOptions"
:tabs="$options.issuableListTabs"
@dismiss-alert="error = undefined"
>
+ <template #nav-actions>
+ <slot name="nav-actions"></slot>
+ </template>
+
+ <template #timeframe="{ issuable = {} }">
+ <issue-card-time-info :issue="issuable" />
+ </template>
+
<template #status="{ issuable }">
{{ getStatus(issuable) }}
</template>
+
+ <template #statistics="{ issuable = {} }">
+ <issue-card-statistics :issue="issuable" />
+ </template>
+
+ <template #list-body>
+ <slot name="list-body"></slot>
+ </template>
</issuable-list>
</template>
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index 5cd38600779..885cea2c1d6 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import WorkItemsListApp from './components/work_items_list_app.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import WorkItemsListApp from 'ee_else_ce/work_items/list/components/work_items_list_app.vue';
export const mountWorkItemsListApp = () => {
const el = document.querySelector('.js-work-items-list-root');
@@ -12,6 +13,13 @@ export const mountWorkItemsListApp = () => {
Vue.use(VueApollo);
+ const {
+ fullPath,
+ hasEpicsFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ } = el.dataset;
+
return new Vue({
el,
name: 'WorkItemsListRoot',
@@ -19,7 +27,10 @@ export const mountWorkItemsListApp = () => {
defaultClient: createDefaultClient(),
}),
provide: {
- fullPath: el.dataset.fullPath,
+ fullPath,
+ hasEpicsFeature: parseBoolean(hasEpicsFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
},
render: (createComponent) => createComponent(WorkItemsListApp),
});
diff --git a/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..1198973d184
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/base_work_item_widgets.fragment.graphql
@@ -0,0 +1,38 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment BaseWorkItemWidgets on WorkItemWidget {
+ ... on WorkItemWidgetAssignees {
+ type
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetLabels {
+ type
+ allowsScopedLabels
+ labels {
+ nodes {
+ id
+ color
+ description
+ title
+ }
+ }
+ }
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ id
+ dueDate
+ startDate
+ title
+ webPath
+ }
+ }
+ ... on WorkItemWidgetStartAndDueDate {
+ type
+ dueDate
+ }
+}
diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
index 7ada2cf12dd..623527302f1 100644
--- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
@@ -1,3 +1,5 @@
+#import "ee_else_ce/work_items/list/queries/work_item_widgets.fragment.graphql"
+
query getWorkItems($fullPath: ID!) {
group(fullPath: $fullPath) {
id
@@ -21,30 +23,7 @@ query getWorkItems($fullPath: ID!) {
updatedAt
webUrl
widgets {
- ... on WorkItemWidgetAssignees {
- assignees {
- nodes {
- id
- avatarUrl
- name
- username
- webUrl
- }
- }
- type
- }
- ... on WorkItemWidgetLabels {
- allowsScopedLabels
- labels {
- nodes {
- id
- color
- description
- title
- }
- }
- type
- }
+ ...WorkItemWidgets
}
workItemType {
id
diff --git a/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
new file mode 100644
index 00000000000..6862df5d330
--- /dev/null
+++ b/app/assets/javascripts/work_items/list/queries/work_item_widgets.fragment.graphql
@@ -0,0 +1,5 @@
+#import "./base_work_item_widgets.fragment.graphql"
+
+fragment WorkItemWidgets on WorkItemWidget {
+ ...BaseWorkItemWidgets
+}
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 49ec12db4e1..b5705b21b5a 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -3,7 +3,11 @@ import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { getPreferredLocales, s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
+import {
+ I18N_WORK_ITEM_ERROR_CREATING,
+ I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
+ sprintfWorkItem,
+} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
@@ -11,9 +15,6 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
export default {
- fetchTypesErrorText: s__(
- 'WorkItem|Something went wrong when fetching work item types. Please try again',
- ),
components: {
GlButton,
GlAlert,
@@ -53,7 +54,7 @@ export default {
}));
},
error() {
- this.error = this.$options.fetchTypesErrorText;
+ this.error = I18N_WORK_ITEM_ERROR_FETCHING_TYPES;
},
},
},
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 5a882977bc2..1443e4b509d 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,9 +1,26 @@
-import { WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_HIERARCHY, WIDGET_TYPE_LABELS } from './constants';
+import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_LABELS,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_START_AND_DUE_DATE,
+ WIDGET_TYPE_WEIGHT,
+} from './constants';
export const isAssigneesWidget = (widget) => widget.type === WIDGET_TYPE_ASSIGNEES;
+export const isHealthStatusWidget = (widget) => widget.type === WIDGET_TYPE_HEALTH_STATUS;
+
export const isLabelsWidget = (widget) => widget.type === WIDGET_TYPE_LABELS;
+export const isMilestoneWidget = (widget) => widget.type === WIDGET_TYPE_MILESTONE;
+
+export const isStartAndDueDateWidget = (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE;
+
+export const isWeightWidget = (widget) => widget.type === WIDGET_TYPE_WEIGHT;
+
export const findHierarchyWidgets = (widgets) =>
widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
@@ -26,3 +43,7 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
+
+export const workItemPath = (fullPath, workItemIid) => {
+ return joinPaths(gon?.relative_url_root || '/', fullPath, '-', 'work_items', workItemIid);
+};
diff --git a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
index 61d93acdb91..57f4783955c 100644
--- a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
+++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
@@ -3,7 +3,8 @@ import { LICENSE_PLAN } from './constants';
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
if (hasSubEpics) {
return LICENSE_PLAN.ULTIMATE;
- } else if (hasEpics) {
+ }
+ if (hasEpics) {
return LICENSE_PLAN.PREMIUM;
}
return LICENSE_PLAN.FREE;
diff --git a/app/assets/stylesheets/disable_animations.scss b/app/assets/stylesheets/disable_animations.scss
index 1e63cdcfa39..7472340896f 100644
--- a/app/assets/stylesheets/disable_animations.scss
+++ b/app/assets/stylesheets/disable_animations.scss
@@ -1,4 +1,7 @@
-* {
+*:not(
+ /* Keep transition enabled where it would otherwise break specs */
+ .always-animate
+) {
/* stylelint-disable property-no-vendor-prefix */
-o-transition: none !important;
-moz-transition: none !important;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 7b8d9281148..514247d2913 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -2,6 +2,7 @@
:root {
--performance-bar-height: 0px;
--system-header-height: 0px;
+ --header-height: 0px;
--top-bar-height: 0px;
--system-footer-height: 0px;
--mr-review-bar-height: 0px;
@@ -22,6 +23,10 @@
--system-header-height: #{$system-header-height};
}
+.with-header {
+ --header-height: #{$header-height};
+}
+
.with-top-bar {
--top-bar-height: #{$top-bar-height};
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 884cb70cb9f..a467d9e8c8a 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -287,6 +287,23 @@
}
}
+ .non-blocking-loader & {
+ &.is-loading{
+ .dropdown-content {
+ display: block;
+ height: 2rem;
+
+ ul{
+ display: none;
+ }
+ }
+ }
+
+ .dropdown-loading{
+ position: relative;
+ }
+ }
+
ul {
margin: 0;
padding: 0;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index b78b07f953b..67e96f08cb0 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -369,22 +369,6 @@
.board-labels-toggle-wrapper {
margin-bottom: $gl-input-padding;
}
-
- .board-swimlanes-toggle-wrapper {
- @include gl-h-auto;
- margin-bottom: $gl-input-padding;
-
- > span,
- > .dropdown,
- .gl-dropdown-toggle {
- @include gl-w-full;
- @include gl-m-0;
- }
-
- > .dropdown {
- @include gl-mt-2;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index b9fbcfb642c..32735679ded 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -5,7 +5,7 @@ $search-input-field-x-min-width: 200px;
padding: 0 16px;
z-index: $header-zindex;
margin-bottom: 0;
- min-height: $header-height;
+ min-height: var(--header-height);
border: 0;
position: fixed;
top: $calc-system-headers-height;
@@ -22,7 +22,7 @@ $search-input-field-x-min-width: 200px;
display: flex;
justify-content: space-between;
position: relative;
- min-height: $header-height;
+ min-height: var(--header-height);
padding-left: 0;
.title {
@@ -505,7 +505,7 @@ $search-input-field-x-min-width: 200px;
.navbar-empty {
justify-content: center;
- height: $header-height;
+ height: var(--header-height);
background: $white;
border-bottom: 1px solid $gray-100;
@@ -642,7 +642,7 @@ header.navbar-gitlab.super-sidebar-logged-out {
&:focus,
&:active {
- box-shadow: inset 0 0 0 $gl-border-size-1 $white;
+ @include gl-focus;
}
&:active {
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
index f77f64f1d76..e409facd081 100644
--- a/app/assets/stylesheets/framework/job_log.scss
+++ b/app/assets/stylesheets/framework/job_log.scss
@@ -6,7 +6,7 @@
word-break: break-all;
word-wrap: break-word;
color: color-yiq($builds-log-bg);
- border-radius: $border-radius-small;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
min-height: 42px;
background-color: $builds-log-bg;
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 086a16edda2..171f070d776 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -4,6 +4,12 @@ html {
&.touch .tooltip {
display: none !important;
}
+
+ @include media-breakpoint-up(sm) {
+ &.logged-out-marketing-header {
+ --header-height: 72px;
+ }
+ }
}
body {
@@ -197,9 +203,3 @@ body {
padding-right: 0;
}
}
-
-@include media-breakpoint-up(sm) {
- .logged-out-marketing-header {
- --header-height: 72px;
- }
-}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index b953ff3024b..b87fd3e67d4 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -76,7 +76,7 @@
.referenced-users {
color: $gl-text-color;
- padding-top: 10px;
+ padding: 0 $gl-padding $gl-padding-8;
}
.referenced-commands {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index edebe9c95ad..a32663b17d3 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -263,7 +263,7 @@
@mixin build-log-top-bar($height) {
@include build-log-bar($height);
position: sticky;
- top: $calc-application-header-height;
+ top: calc(#{$calc-application-header-height} - 1px);
}
/*
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 5f90dd62426..f2afa94e000 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -231,18 +231,6 @@
}
}
-.health-status {
- .dropdown-body {
- .health-divider {
- border-top-color: $gray-100;
- }
-
- .dropdown-item:not(.health-dropdown-item) {
- padding: 0;
- }
- }
-}
-
.toggle-right-sidebar-button {
@include side-panel-toggle;
border-bottom: 1px solid $border-color;
@@ -799,29 +787,6 @@
}
}
-.participants-more,
-.user-list-more {
- margin-left: 5px;
-
- a,
- .btn-link {
- color: $gl-text-color-secondary;
- }
-
- .btn-link {
- padding: 0;
- }
-
- .btn-link:hover {
- color: $blue-800;
- text-decoration: none;
- }
-
- .btn-link:focus {
- text-decoration: none;
- }
-}
-
.sidebar-help-wrap {
.sidebar-help-state {
margin: 16px -20px -20px;
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 8610c41b43f..fbf9d8c8ca6 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -104,27 +104,6 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
box-shadow: none;
}
- .context-switcher .gl-new-dropdown-custom-toggle {
- width: 100%;
- }
-
- .context-switcher .gl-new-dropdown-panel {
- overflow-y: auto;
- }
-
- .context-switcher-search-box input {
- @include gl-font-sm;
- }
-
- .gl-new-dropdown-custom-toggle .context-switcher-toggle {
- &[aria-expanded='true'] {
- background-color: $t-gray-a-08;
- }
-
- &:focus {
- @include gl-focus($inset: true); }
- }
-
.btn-with-notification {
position: relative;
@@ -158,15 +137,6 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
- .nav-item-link {
- &:hover,
- &:focus-within {
- .nav-item-badge {
- opacity: 0;
- }
- }
- }
-
#trial-status-sidebar-widget:hover {
text-decoration: none;
@include gl-text-contrast-light;
@@ -177,6 +147,15 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
display: none;
}
+.super-sidebar-has-peeked {
+ margin-top: calc(#{$header-height} - #{$gl-spacing-scale-2});
+ margin-bottom: #{$gl-spacing-scale-2};
+}
+
+.super-sidebar-peek {
+ margin-left: #{$gl-spacing-scale-2};
+}
+
.super-sidebar-peek,
.super-sidebar-peek-hint {
@include gl-shadow;
@@ -189,6 +168,14 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
+.super-sidebar-peek {
+ border-radius: $border-radius-default;
+
+ .user-bar {
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+}
+
.page-with-super-sidebar {
padding-left: 0;
@@ -295,19 +282,71 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
+.transition-opacity-on-hover--context {
+ .transition-opacity-on-hover--target {
+ transition: opacity $gl-transition-duration-fast linear;
+
+ &:hover {
+ transition-delay: $gl-transition-duration-slow;
+ }
+ }
+
+ &:hover {
+ .transition-opacity-on-hover--target {
+ transition-delay: $gl-transition-duration-slow;
+ }
+ }
+}
+
.show-on-focus-or-hover--context {
.show-on-focus-or-hover--target {
opacity: 0;
+
+ &:hover,
+ &:focus {
+ opacity: 1;
+ }
}
&:hover,
- &:focus {
+ &:focus-within {
+ .show-on-focus-or-hover--control {
+ @include gl-bg-t-gray-a-08;
+ }
+
.show-on-focus-or-hover--target {
opacity: 1;
}
}
- .show-on-focus-or-hover--target:focus {
+ .show-on-focus-or-hover--control {
+ &:hover,
+ &:focus {
+ + .show-on-focus-or-hover--target {
+ opacity: 1;
+ }
+ }
+ }
+}
+
+.hide-on-focus-or-hover--context {
+ .hide-on-focus-or-hover--target {
opacity: 1;
}
+
+ &:hover,
+ &:focus-within {
+ .hide-on-focus-or-hover--target {
+ opacity: 0;
+ }
+ }
+
+ .hide-on-focus-or-hover--control {
+ &:hover,
+ &:focus {
+ .hide-on-focus-or-hover--target {
+ opacity: 0;
+ }
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d632689a4f6..e83f6af603a 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -283,75 +283,79 @@ $color-ranges: (
// GitLab themes
-$indigo-50: #f7f7ff;
-$indigo-100: #ebebfa;
-$indigo-200: #d1d1f0;
-$indigo-300: #a6a6de;
-$indigo-400: #7c7ccc;
+$indigo-50: #f1f1ff;
+$indigo-100: #dbdbf8;
+$indigo-200: #c7c7f2;
+$indigo-300: #a2a2e6;
+$indigo-400: #8181d7;
$indigo-500: #6666c4;
-$indigo-600: #5b5bbd;
-$indigo-700: #4b4ba3;
-$indigo-800: #393982;
-$indigo-900: #292961;
-$indigo-950: #1a1a40;
+$indigo-600: #5252b5;
+$indigo-700: #41419f;
+$indigo-800: #303083;
+$indigo-900: #222261;
+$indigo-950: #14143d;
// To do this variant right for darkmode, we need to create a variable for it.
$indigo-900-alpha-008: rgba($indigo-900, 0.08);
-$theme-blue-50: #f4f8fc;
-$theme-blue-100: #e6edf5;
-$theme-blue-200: #c8d7e6;
-$theme-blue-300: #97b3cf;
-$theme-blue-400: #648cb4;
-$theme-blue-500: #4a79a8;
-$theme-blue-600: #3e6fa0;
-$theme-blue-700: #305c88;
-$theme-blue-800: #25496e;
-$theme-blue-900: #1a3652;
-$theme-blue-950: #0f2235;
-
-$theme-light-blue-50: #f2f7fc;
-$theme-light-blue-100: #ebf1f7;
-$theme-light-blue-200: #c9dcf2;
-$theme-light-blue-300: #83abd4;
-$theme-light-blue-400: #4d86bf;
-$theme-light-blue-500: #367cc2;
-$theme-light-blue-600: #3771ab;
-$theme-light-blue-700: #2261a1;
-
-$theme-green-50: #f2faf6;
-$theme-green-100: #e4f3ea;
-$theme-green-200: #c0dfcd;
-$theme-green-300: #8ac2a1;
-$theme-green-400: #52a274;
-$theme-green-500: #35935c;
-$theme-green-600: #288a50;
-$theme-green-700: #1c7441;
-$theme-green-800: #145d33;
-$theme-green-900: #0d4524;
-$theme-green-950: #072d16;
-
-$theme-light-green-700: #156b39;
-
-$theme-red-50: #fcf4f2;
-$theme-red-100: #fae9e6;
-$theme-red-200: #ebcac5;
-$theme-red-300: #d99b91;
-$theme-red-400: #b0655a;
+$theme-blue-50: #cdd8e3;
+$theme-blue-100: #b9cadc;
+$theme-blue-200: #a6bdd5;
+$theme-blue-300: #81a5c9;
+$theme-blue-400: #628eb9;
+$theme-blue-500: #4977a5;
+$theme-blue-600: #346596;
+$theme-blue-700: #235180;
+$theme-blue-800: #153c63;
+$theme-blue-900: #0b2640;
+$theme-blue-950: #04101c;
+
+$theme-light-blue-50: #dde6ee;
+$theme-light-blue-100: #c1d4e6;
+$theme-light-blue-200: #a0bedc;
+$theme-light-blue-300: #74a3d3;
+$theme-light-blue-400: #4f8bc7;
+$theme-light-blue-500: #3476b9;
+$theme-light-blue-600: #2268ae;
+$theme-light-blue-700: #145aa1;
+$theme-light-blue-800: #0e4d8d;
+$theme-light-blue-900: #0c4277;
+$theme-light-blue-950: #0a3764;
+
+$theme-green-50: #dde9de;
+$theme-green-100: #b1d6b5;
+$theme-green-200: #8cc497;
+$theme-green-300: #69af7d;
+$theme-green-400: #499767;
+$theme-green-500: #308258;
+$theme-green-600: #25744c;
+$theme-green-700: #1b653f;
+$theme-green-800: #155635;
+$theme-green-900: #0e4328;
+$theme-green-950: #052e19;
+
+$theme-red-50: #f4e9e7;
+$theme-red-100: #ecd3d0;
+$theme-red-200: #e3bab5;
+$theme-red-300: #d59086;
+$theme-red-400: #c66e60;
$theme-red-500: #ad4a3b;
-$theme-red-600: #9e4133;
-$theme-red-700: #912f20;
-$theme-red-800: #78291d;
-$theme-red-900: #691a16;
-$theme-red-950: #36140f;
-
-$theme-light-red-50: #fff6f5;
-$theme-light-red-100: #fae2de;
-$theme-light-red-200: #f7d5d0;
-$theme-light-red-300: #d9796a;
-$theme-light-red-400: #cf604e;
+$theme-red-600: #a13322;
+$theme-red-700: #8f2110;
+$theme-red-800: #761405;
+$theme-red-900: #580d02;
+$theme-red-950: #380700;
+
+$theme-light-red-50: #faf2f1;
+$theme-light-red-100: #f6d9d5;
+$theme-light-red-200: #ebada2;
+$theme-light-red-300: #e07f6f;
+$theme-light-red-400: #d36250;
$theme-light-red-500: #c24b38;
-$theme-light-red-600: #b03927;
-$theme-light-red-700: #a62e21;
+$theme-light-red-600: #b53a26;
+$theme-light-red-700: #a02e1c;
+$theme-light-red-800: #8b2212;
+$theme-light-red-900: #751709;
+$theme-light-red-950: #5c1105;
// Data visualization color palette
@@ -459,7 +463,7 @@ $browser-scrollbar-size: 10px;
/*
* Misc
*/
-$header-height: var(--header-height, 48px);
+$header-height: 48px;
$content-wrapper-padding: 100px;
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
@@ -501,7 +505,7 @@ $pages-group-name-color: #4c4e54;
* Calculated heights
*/
$calc-system-headers-height: calc(var(--system-header-height) + var(--performance-bar-height));
-$calc-application-bars-height: calc(#{$header-height} + #{$calc-system-headers-height});
+$calc-application-bars-height: calc(var(--header-height) + #{$calc-system-headers-height});
$calc-application-header-height: calc(#{$calc-application-bars-height} + var(--top-bar-height));
$calc-application-footer-height: var(--system-footer-height);
$calc-application-viewport-height: calc(100vh - #{$calc-application-header-height} - #{$calc-application-footer-height});
@@ -692,14 +696,6 @@ $ci-skipped-color: #888;
*/
$issue-boards-font-size: 14px;
$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
-/*
- The following heights are used in environment_logs.scss and are used for calculation of the log viewer height.
-*/
-$environment-logs-breadcrumbs-height: 63px;
-$environment-logs-breadcrumbs-height-md: $top-bar-height;
-
-$environment-logs-difference-xs-up: calc(#{$header-height} + #{$environment-logs-breadcrumbs-height});
-$environment-logs-difference-md-up: calc(#{$header-height} + #{$environment-logs-breadcrumbs-height-md});
/*
* Avatar
@@ -845,12 +841,6 @@ $perf-bar-bucket-box-shadow-to: rgba($black, 0.25);
$perf-bar-canary-text: $orange-400;
/*
-Issuable warning
-*/
-$issuable-warning-size: 24px;
-$issuable-warning-icon-margin: 4px;
-
-/*
Image Commenting cursor
*/
$image-comment-cursor-left-offset: 12;
diff --git a/app/assets/stylesheets/page_bundles/admin/jobs_index.scss b/app/assets/stylesheets/page_bundles/admin/jobs_index.scss
deleted file mode 100644
index 7844cae5f87..00000000000
--- a/app/assets/stylesheets/page_bundles/admin/jobs_index.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.admin-builds-table {
- td:last-child {
- min-width: 120px;
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 5114f484e53..09c4d184f3f 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -15,6 +15,9 @@
.top-bar {
@include build-log-top-bar(50px);
+ z-index: 2;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ box-shadow: 0 -2px 0 0 var(--white);
&.has-archived-block {
top: calc(#{$calc-application-header-height} + 28px);
@@ -86,8 +89,6 @@
}
.right-sidebar.build-sidebar {
- padding: 0;
-
&.right-sidebar-collapsed {
display: none;
}
@@ -100,29 +101,6 @@
-webkit-overflow-scrolling: touch;
}
- .blocks-container {
- padding: 0 $gl-padding;
- width: 289px;
- }
-
- .trigger-variables-btn-container {
- justify-content: space-between;
- align-items: center;
-
- .trigger-variables-btn {
- margin-top: -5px;
- margin-bottom: -5px;
- }
- }
-
- .trigger-build-variables {
- margin: 0;
- overflow-x: auto;
- width: 100%;
- -ms-overflow-style: scrollbar;
- -webkit-overflow-scrolling: touch;
- }
-
.trigger-build-variable {
font-weight: $gl-font-weight-normal;
color: var(--gray-950, $gray-950);
@@ -142,38 +120,20 @@
vertical-align: top;
}
- .badge.badge-pill {
- margin-left: 2px;
+ .blocks-container {
+ width: 289px;
}
- .stage-item {
- cursor: pointer;
-
- &:hover {
- color: var(--gl-text-color, $gl-text-color);
- }
+ .block {
+ width: 262px;
}
.builds-container {
- background-color: var(--white, $white);
- border-top: 1px solid var(--border-color, $border-color);
- border-bottom: 1px solid var(--border-color, $border-color);
- max-height: 300px;
- width: 289px;
overflow: auto;
- a {
- padding: $gl-padding 10px $gl-padding 40px;
- width: 270px;
-
- &:hover {
- color: var(--gl-text-color, $gl-text-color);
- }
- }
-
.icon-arrow-right {
- left: 15px;
- top: 20px;
+ left: 8px;
+ top: 12px;
}
.build-job {
@@ -192,9 +152,15 @@
.container-fluid.container-limited {
max-width: 100%;
}
+}
- .content-wrapper {
- padding-bottom: 6px;
+.build-sidebar-item {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ grid-gap: $gl-padding-8;
+
+ &:last-of-type {
+ @include gl-mb-0;
}
}
@@ -205,12 +171,3 @@
margin-bottom: 0;
}
}
-
-@include media-breakpoint-down(md) {
- .content-list {
- &.builds-content-list {
- width: 100%;
- overflow: auto;
- }
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
index fde35ab3d39..c7873473b86 100644
--- a/app/assets/stylesheets/page_bundles/incidents.scss
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -43,8 +43,7 @@
}
}
- &:last-child,
- &.create-timeline-event {
+ &:last-child {
&::before {
top: - #{$gl-spacing-scale-5} !important; // Override default positioning
@include gl-h-8;
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 1b5da0368c6..5397f3d8895 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -1,15 +1,6 @@
@import 'mixins_and_variables_and_functions';
-.status-box {
- padding: 0 $gl-btn-padding;
- border-radius: $border-radius-default;
- display: block;
- float: left;
- margin-right: $gl-padding-8;
- color: var(--white, $white);
- font-size: $gl-font-size;
- line-height: $gl-line-height-24;
-}
+$issuable-warning-size: 24px;
.issuable-warning-icon {
background-color: var(--orange-50, $orange-50);
@@ -18,7 +9,6 @@
width: $issuable-warning-size;
height: $issuable-warning-size;
text-align: center;
- margin-right: $issuable-warning-icon-margin;
line-height: $gl-line-height-24;
flex: 0 0 auto;
}
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index 113a50c4efa..f03efb82860 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -3,6 +3,12 @@
$tabs-holder-z-index: 250;
$comparison-empty-state-height: 62px;
+.apply-suggestions-input-min-width {
+ @include media-breakpoint-up(lg) {
+ width: $gl-dropdown-width-wide;
+ }
+}
+
.space-children {
@include clearfix;
@@ -199,6 +205,7 @@ $comparison-empty-state-height: 62px;
top: $calc-application-header-height;
z-index: $tabs-holder-z-index;
border-bottom: 1px solid var(--border-color, $border-color);
+ background-color: var(--gray-10, $white);
@include media-breakpoint-up(md) {
position: sticky;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index f39247f06c2..b00e1813696 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -372,7 +372,6 @@ $tabs-holder-z-index: 250;
white-space: nowrap;
}
- /* stylelint-disable scss/at-rule-no-unknown */
@container mr-widget-extension (max-width: 600px) {
flex-direction: column;
align-items: flex-start;
@@ -417,7 +416,7 @@ $tabs-holder-z-index: 250;
.media-body {
min-width: 0;
- font-size: 12px;
+ font-size: $gl-font-size-sm;
margin-left: 32px;
}
@@ -548,6 +547,7 @@ $tabs-holder-z-index: 250;
}
.mr-widget-section:not(:first-child) > div,
+ .mr-widget-section:not(:first-child) > section,
.mr-widget-section .mr-widget-section > div {
border-top: solid 1px var(--border-color, $border-color);
}
@@ -649,7 +649,6 @@ $tabs-holder-z-index: 250;
.label-branch {
@include gl-font-monospace;
- font-size: 95%;
overflow: hidden;
word-break: break-all;
}
@@ -663,7 +662,7 @@ $tabs-holder-z-index: 250;
> span {
display: inline-block;
max-width: 12.5em;
- margin-bottom: -5px;
+ margin-bottom: px-to-rem(-5px);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
@@ -987,6 +986,14 @@ $tabs-holder-z-index: 250;
}
}
+.container-fluid.diffs-container-limited {
+ .flash-container {
+ @include gl-mx-auto;
+ @include gl-max-w-container-xl;
+ @include gl-px-5;
+ }
+}
+
.submit-review-dropdown {
&.show .dropdown-menu {
width: calc(100vw - 20px);
diff --git a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
deleted file mode 100644
index af2dac7739e..00000000000
--- a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
+++ /dev/null
@@ -1,82 +0,0 @@
-@import 'mixins_and_variables_and_functions';
-
-.ci-variable-list {
- margin-left: 0;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- clear: both;
-}
-
-.ci-variable-row {
- display: flex;
- align-items: flex-start;
-
- @include media-breakpoint-down(xs) {
- align-items: flex-end;
- }
-
- &:not(:last-child) {
- margin-bottom: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-bottom: 3 * $gl-btn-padding;
- }
- }
-
- &:last-child {
- .ci-variable-body-item:last-child {
- margin-right: $ci-variable-remove-button-width;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- }
- }
-
- .ci-variable-row-remove-button {
- display: none;
- }
-
- @include media-breakpoint-down(xs) {
- .ci-variable-row-body {
- margin-right: $ci-variable-remove-button-width;
- }
- }
- }
-}
-
-.ci-variable-row-body {
- display: flex;
- align-items: flex-start;
- width: 100%;
- padding-bottom: $gl-padding;
-
- @include media-breakpoint-down(xs) {
- display: block;
- }
-}
-
-.ci-variable-body-item {
- flex: 1;
-
- &:not(:last-child) {
- margin-right: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- margin-bottom: $gl-btn-padding;
- }
- }
-}
-
-.pipeline-schedule-form {
- .gl-field-error {
- margin: 10px 0 0;
- }
-}
-
-.pipeline-schedule-table-row {
- a {
- color: var(--gl-text-color, $gl-text-color);
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/profiles/preferences.scss b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
index 1a59f96c6ee..d09ad42a722 100644
--- a/app/assets/stylesheets/page_bundles/profiles/preferences.scss
+++ b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
@@ -1,9 +1,9 @@
@import 'page_bundles/mixins_and_variables_and_functions';
.application-theme {
- $ui-gray-bg: #303030;
- $ui-light-gray-bg: #f0f0f0;
- $ui-dark-mode-bg: #1f1f1f;
+ $ui-gray-bg: $gray-900;
+ $ui-light-gray-bg: $gray-50;
+ $ui-dark-mode-bg: $gray-950;
.preview {
font-size: 0;
@@ -33,7 +33,7 @@
}
&.ui-light-green {
- background-color: $theme-light-green-700;
+ background-color: $theme-green-700;
}
&.ui-red {
diff --git a/app/assets/stylesheets/page_bundles/projects_usage_quotas.scss b/app/assets/stylesheets/page_bundles/projects_usage_quotas.scss
deleted file mode 100644
index 8f2cbc402c9..00000000000
--- a/app/assets/stylesheets/page_bundles/projects_usage_quotas.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-@import 'mixins_and_variables_and_functions';
-
-.storage-type-usage {
- &:first-child {
- @include gl-rounded-top-left-base;
- @include gl-rounded-bottom-left-base;
- }
-
- &:last-child {
- @include gl-rounded-top-right-base;
- @include gl-rounded-bottom-right-base;
- }
-
- &:not(:last-child) {
- @include gl-border-r-2;
- @include gl-border-r-solid;
- border-right-color: var(--white, $white);
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index e8fa93e1504..f36cbc129a7 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -173,3 +173,13 @@ $work-item-sticky-header-height: 52px;
min-width: 100%;
}
}
+
+.work-item-notification-toggle {
+ .gl-toggle {
+ margin-left: auto;
+ }
+
+ .gl-toggle-label {
+ font-weight: normal;
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 0c9d151e3cd..38686d5e713 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -136,7 +136,7 @@
}
.icon {
- margin-right: $issuable-warning-icon-margin;
+ margin-right: $gl-padding-4;
vertical-align: text-bottom;
fill: $orange-600;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index b9cae28537d..9ce470dbcf2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -306,8 +306,6 @@
}
.project-details {
- max-width: 625px;
-
p,
.commit-row-message {
@include gl-mb-0;
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 030e41046d3..7616f573412 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -235,7 +235,7 @@ body.gl-dark {
}
- .navbar-gitlab {
+ .navbar.navbar-gitlab {
background-color: var(--gray-50);
box-shadow: 0 1px 0 0 var(--gray-100);
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index 8f0e0781918..db20034419a 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -20,7 +20,7 @@
// Header
- .navbar-gitlab {
+ .navbar-gitlab:not(.super-sidebar-logged-out) {
background-color: $navbar-theme-color;
.navbar-collapse {
@@ -283,11 +283,12 @@
$theme-color,
$theme-color-darkest,
) {
+ --sidebar-background: #{mix(white, $theme-color-lightest, 50%)};
--transparent-white-16: rgba(255, 255, 255, 0.16);
--transparent-white-24: rgba(255, 255, 255, 0.24);
.super-sidebar {
- background-color: $theme-color-lightest;
+ background-color: var(--sidebar-background);
}
.super-sidebar .user-bar {
@@ -335,4 +336,8 @@
.active-indicator {
background-color: $theme-color;
}
+
+ .super-sidebar-context-header {
+ color: $theme-color;
+ }
}
diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index 9b7fc10e769..e8357647f48 100644
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -10,7 +10,7 @@ body {
$gray-500
);
- .navbar-gitlab {
+ .navbar-gitlab:not(.super-sidebar-logged-out) {
background-color: $gray-50;
box-shadow: 0 1px 0 0 $border-color;
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index 720a0ec58b8..6b058b2dd7b 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -6,7 +6,7 @@ body {
$theme-green-200,
$theme-green-500,
$theme-green-500,
- $theme-light-green-700,
+ $theme-green-700,
$white
);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index d5e9d35983a..b756e0ed704 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -132,19 +132,6 @@
fill: $red-500;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3569
-.gl-mb-n5 {
- margin-bottom: -$gl-spacing-scale-5;
-}
-
-.gl-mb-n7 {
- margin-bottom: -$gl-spacing-scale-7;
-}
-
-.gl-mb-n8 {
- margin-bottom: -$gl-spacing-scale-8;
-}
-
.gl-hover-border-gray-100:hover {
border-color: $gray-100;
}