summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue7
-rw-r--r--app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js2
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete.vue4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue48
-rw-r--r--app/assets/javascripts/admin/users/components/associations/associations_list.vue65
-rw-r--r--app/assets/javascripts/admin/users/components/associations/associations_list_item.vue27
-rw-r--r--app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue17
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue1
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue2
-rw-r--r--app/assets/javascripts/analytics/shared/components/daterange.vue4
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue2
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue36
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js28
-rw-r--r--app/assets/javascripts/api/groups_api.js8
-rw-r--r--app/assets/javascripts/api/user_api.js6
-rw-r--r--app/assets/javascripts/artifacts/components/artifact_delete_modal.vue54
-rw-r--r--app/assets/javascripts/artifacts/components/artifact_row.vue87
-rw-r--r--app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue118
-rw-r--r--app/assets/javascripts/artifacts/components/job_artifacts_table.vue337
-rw-r--r--app/assets/javascripts/artifacts/constants.js55
-rw-r--r--app/assets/javascripts/artifacts/graphql/cache_update.js30
-rw-r--r--app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql7
-rw-r--r--app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql57
-rw-r--r--app/assets/javascripts/artifacts/index.js29
-rw-r--r--app/assets/javascripts/artifacts/utils.js26
-rw-r--r--app/assets/javascripts/behaviors/copy_code.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js2
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js129
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js7
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js4
-rw-r--r--app/assets/javascripts/blob/blob_blame_link.js14
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js96
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js6
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_content.vue66
-rw-r--r--app/assets/javascripts/blob/utils.js26
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js22
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js53
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_card_move_to_position.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue99
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue85
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue20
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue10
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue68
-rw-r--r--app/assets/javascripts/boards/graphql.js15
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js8
-rw-r--r--app/assets/javascripts/boards/stores/actions.js5
-rw-r--r--app/assets/javascripts/branches/components/delete_merged_branches.vue171
-rw-r--r--app/assets/javascripts/branches/init_delete_merged_branches.js23
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue45
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue256
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue (renamed from app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue (renamed from app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue (renamed from app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue (renamed from app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue (renamed from app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue (renamed from app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue (renamed from app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue)7
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal.vue54
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue (renamed from app/assets/javascripts/pipeline_schedules/components/take_ownership_modal.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql (renamed from app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql12
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql (renamed from app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql)5
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js (renamed from app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js (renamed from app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js)0
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue (renamed from app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/index.js (renamed from app/assets/javascripts/runner/admin_runner_show/index.js)0
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue (renamed from app/assets/javascripts/runner/admin_runners/admin_runners_app.vue)12
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/index.js (renamed from app/assets/javascripts/runner/admin_runners/index.js)2
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/link_cell.vue (renamed from app/assets/javascripts/runner/components/cells/link_cell.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_actions_cell.vue (renamed from app/assets/javascripts/runner/components/cells/runner_actions_cell.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue (renamed from app/assets/javascripts/runner/components/cells/runner_owner_cell.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue (renamed from app/assets/javascripts/runner/components/cells/runner_stacked_summary_cell.vue)2
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue (renamed from app/assets/javascripts/runner/components/cells/runner_status_cell.vue)2
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue (renamed from app/assets/javascripts/runner/components/cells/runner_summary_field.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue (renamed from app/assets/javascripts/runner/components/registration/registration_dropdown.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_token.vue (renamed from app/assets/javascripts/runner/components/registration/registration_token.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue (renamed from app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue)4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_assigned_item.vue (renamed from app/assets/javascripts/runner/components/runner_assigned_item.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue (renamed from app/assets/javascripts/runner/components/runner_bulk_delete.vue)35
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_bulk_delete_checkbox.vue (renamed from app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_button.vue (renamed from app/assets/javascripts/runner/components/runner_delete_button.vue)11
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_delete_modal.vue (renamed from app/assets/javascripts/runner/components/runner_delete_modal.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_detail.vue (renamed from app/assets/javascripts/runner/components/runner_detail.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_details.vue (renamed from app/assets/javascripts/runner/components/runner_details.vue)6
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_edit_button.vue (renamed from app/assets/javascripts/runner/components/runner_edit_button.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue (renamed from app/assets/javascripts/runner/components/runner_filtered_search_bar.vue)2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_groups.vue (renamed from app/assets/javascripts/runner/components/runner_groups.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_header.vue (renamed from app/assets/javascripts/runner/components/runner_header.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs.vue (renamed from app/assets/javascripts/runner/components/runner_jobs.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue (renamed from app/assets/javascripts/runner/components/runner_jobs_table.vue)29
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list.vue (renamed from app/assets/javascripts/runner/components/runner_list.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue (renamed from app/assets/javascripts/runner/components/runner_list_empty_state.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_membership_toggle.vue (renamed from app/assets/javascripts/runner/components/runner_membership_toggle.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_name.vue (renamed from app/assets/javascripts/runner/components/runner_name.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_pagination.vue (renamed from app/assets/javascripts/runner/components/runner_pagination.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_pause_button.vue (renamed from app/assets/javascripts/runner/components/runner_pause_button.vue)4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_paused_badge.vue (renamed from app/assets/javascripts/runner/components/runner_paused_badge.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_projects.vue (renamed from app/assets/javascripts/runner/components/runner_projects.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_status_badge.vue (renamed from app/assets/javascripts/runner/components/runner_status_badge.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_status_popover.vue (renamed from app/assets/javascripts/runner/components/runner_status_popover.vue)2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_tag.vue (renamed from app/assets/javascripts/runner/components/runner_tag.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_tags.vue (renamed from app/assets/javascripts/runner/components/runner_tags.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_badge.vue (renamed from app/assets/javascripts/runner/components/runner_type_badge.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_tabs.vue (renamed from app/assets/javascripts/runner/components/runner_type_tabs.vue)2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_update_form.vue (renamed from app/assets/javascripts/runner/components/runner_update_form.vue)8
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js (renamed from app/assets/javascripts/runner/components/search_tokens/paused_token_config.js)0
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js (renamed from app/assets/javascripts/runner/components/search_tokens/status_token_config.js)8
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue (renamed from app/assets/javascripts/runner/components/search_tokens/tag_token.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js (renamed from app/assets/javascripts/runner/components/search_tokens/tag_token_config.js)0
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/upgrade_status_token_config.js (renamed from app/assets/javascripts/runner/components/search_tokens/upgrade_status_token_config.js)0
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_count.vue (renamed from app/assets/javascripts/runner/components/stat/runner_count.vue)4
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_single_stat.vue (renamed from app/assets/javascripts/runner/components/stat/runner_single_stat.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_stats.vue (renamed from app/assets/javascripts/runner/components/stat/runner_stats.vue)4
-rw-r--r--app/assets/javascripts/ci/runner/constants.js (renamed from app/assets/javascripts/runner/constants.js)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_fields.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_form.query.graphql (renamed from app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/edit/runner_update.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql (renamed from app/assets/javascripts/runner/graphql/list/all_runners.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners_connection.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/list/all_runners_connection.fragment.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql (renamed from app/assets/javascripts/runner/graphql/list/all_runners_count.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/list/bulk_runner_delete.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/checked_runner_ids.query.graphql (renamed from app/assets/javascripts/runner/graphql/list/checked_runner_ids.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/group_runner_connection.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/list/group_runner_connection.fragment.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/group_runners.query.graphql (renamed from app/assets/javascripts/runner/graphql/list/group_runners.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/group_runners_count.query.graphql (renamed from app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/local_state.js (renamed from app/assets/javascripts/runner/graphql/list/local_state.js)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/list/runners_registration_token_reset.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/typedefs.graphql (renamed from app/assets/javascripts/runner/graphql/list/typedefs.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/shared/runner_delete.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/shared/runner_delete.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/shared/runner_toggle_active.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner.query.graphql (renamed from app/assets/javascripts/runner/graphql/show/runner.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql (renamed from app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql (renamed from app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql)4
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_projects.query.graphql (renamed from app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue (renamed from app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/group_runner_show/index.js (renamed from app/assets/javascripts/runner/group_runner_show/index.js)0
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue (renamed from app/assets/javascripts/runner/group_runners/group_runners_app.vue)19
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/index.js (renamed from app/assets/javascripts/runner/group_runners/index.js)0
-rw-r--r--app/assets/javascripts/ci/runner/local_storage_alert/constants.js (renamed from app/assets/javascripts/runner/local_storage_alert/constants.js)0
-rw-r--r--app/assets/javascripts/ci/runner/local_storage_alert/save_alert_to_local_storage.js (renamed from app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js)0
-rw-r--r--app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js (renamed from app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js)0
-rw-r--r--app/assets/javascripts/ci/runner/runner_edit/index.js (renamed from app/assets/javascripts/runner/runner_edit/index.js)0
-rw-r--r--app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue (renamed from app/assets/javascripts/runner/runner_edit/runner_edit_app.vue)0
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js (renamed from app/assets/javascripts/runner/runner_search_utils.js)0
-rw-r--r--app/assets/javascripts/ci/runner/runner_update_form_utils.js (renamed from app/assets/javascripts/runner/runner_update_form_utils.js)0
-rw-r--r--app/assets/javascripts/ci/runner/sentry_utils.js (renamed from app/assets/javascripts/runner/sentry_utils.js)0
-rw-r--r--app/assets/javascripts/ci/runner/utils.js (renamed from app/assets/javascripts/runner/utils.js)0
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue139
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue132
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue159
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue62
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue58
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue6
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue232
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue116
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue81
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue428
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue32
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue199
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql11
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql11
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql11
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql11
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql6
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql6
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/settings.js60
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js62
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js208
-rw-r--r--app/assets/javascripts/ci_variable_list/store/getters.js6
-rw-r--r--app/assets/javascripts/ci_variable_list/store/index.js19
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutation_types.js33
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js128
-rw-r--r--app/assets/javascripts/ci_variable_list/store/state.js26
-rw-r--r--app/assets/javascripts/ci_variable_list/store/utils.js45
-rw-r--r--app/assets/javascripts/clusters_list/components/delete_agent_button.vue7
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue10
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue8
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue2
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/highlight.js19
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js1
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js2
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js2
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/filter_bar.vue13
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue17
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js25
-rw-r--r--app/assets/javascripts/diffs/store/actions.js14
-rw-r--r--app/assets/javascripts/diffs/store/utils.js11
-rw-r--r--app/assets/javascripts/diffs/utils/tree_worker_utils.js9
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js2
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js58
-rw-r--r--app/assets/javascripts/editor/schema/ci.json214
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue3
-rw-r--r--app/assets/javascripts/environments/components/environment_rollback.vue15
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql1
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js3
-rw-r--r--app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js3
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js24
-rw-r--r--app/assets/javascripts/flash.js3
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js43
-rw-r--r--app/assets/javascripts/gitlab_version_check.js20
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue73
-rw-r--r--app/assets/javascripts/gitlab_version_check/constants.js9
-rw-r--r--app/assets/javascripts/gitlab_version_check/index.js50
-rw-r--r--app/assets/javascripts/gl_field_errors.js11
-rw-r--r--app/assets/javascripts/google_cloud/service_accounts/list.vue12
-rw-r--r--app/assets/javascripts/google_tag_manager/index.js29
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js38
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json7
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue5
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue52
-rw-r--r--app/assets/javascripts/groups/init_transfer_group_form.js22
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue282
-rw-r--r--app/assets/javascripts/groups_select.js37
-rw-r--r--app/assets/javascripts/ide/components/ide.vue14
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue15
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue17
-rw-r--r--app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue103
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue19
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js2
-rw-r--r--app/assets/javascripts/integrations/constants.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue19
-rw-r--r--app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue22
-rw-r--r--app/assets/javascripts/integrations/edit/components/trigger_fields.vue4
-rw-r--r--app/assets/javascripts/integrations/edit/index.js4
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue36
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue164
-rw-r--r--app/assets/javascripts/invite_members/components/user_limit_notification.vue105
-rw-r--r--app/assets/javascripts/invite_members/constants.js16
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql5
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue171
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js31
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js33
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue1
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue56
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js25
-rw-r--r--app/assets/javascripts/issues/index.js4
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue56
-rw-r--r--app/assets/javascripts/issues/list/constants.js37
-rw-r--r--app/assets/javascripts/issues/list/graphql.js25
-rw-r--r--app/assets/javascripts/issues/list/index.js27
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql3
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql7
-rw-r--r--app/assets/javascripts/issues/list/utils.js33
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/fields/type.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue29
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue9
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue13
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/duration_cell.vue27
-rw-r--r--app/assets/javascripts/jobs/constants.js1
-rw-r--r--app/assets/javascripts/lazy_loader.js3
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js60
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js57
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js78
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js19
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js12
-rw-r--r--app/assets/javascripts/lib/utils/unit_format/index.js17
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue9
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue16
-rw-r--r--app/assets/javascripts/members/index.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js41
-rw-r--r--app/assets/javascripts/milestones/milestone_select.js273
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/experiment.vue36
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue48
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue7
-rw-r--r--app/assets/javascripts/notebook/cells/output/index.vue9
-rw-r--r--app/assets/javascripts/notebook/cells/output/markdown.vue42
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue7
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue7
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue12
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js14
-rw-r--r--app/assets/javascripts/notes/stores/getters.js6
-rw-r--r--app/assets/javascripts/observability/components/observability_app.vue42
-rw-r--r--app/assets/javascripts/observability/index.js28
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue22
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue9
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js18
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue46
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue61
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue57
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue112
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql16
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/index.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue62
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue98
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue91
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue190
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js56
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql15
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_package_forwarding_settings.mutation.graphql16
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql7
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/delete_package_modal.vue83
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js6
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue47
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/constants.js11
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue31
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/runners/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/runners/index/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/runners/show/index.js2
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/dashboards/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/explore/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/observability/manage/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/runners/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/runners/index/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/runners/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js13
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_tabs.js136
-rw-r--r--app/assets/javascripts/pages/profiles/init_timezone_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/hooks/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js3
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue1
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js4
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/show/index.js31
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js11
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue25
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js2
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue2
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue1
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue134
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue62
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue16
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue8
-rw-r--r--app/assets/javascripts/pipelines/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js10
-rw-r--r--app/assets/javascripts/pipelines/pipeline_tabs.js15
-rw-r--r--app/assets/javascripts/pipelines/routes.js20
-rw-r--r--app/assets/javascripts/pipelines/utils.js17
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue2
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js3
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue4
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_button.vue17
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue2
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue26
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js4
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js3
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js9
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue43
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue14
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue13
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js3
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue161
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js6
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue10
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue62
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql15
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue23
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue56
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue10
-rw-r--r--app/assets/javascripts/related_issues/constants.js5
-rw-r--r--app/assets/javascripts/related_issues/index.js1
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue4
-rw-r--r--app/assets/javascripts/releases/components/asset_links_form.vue16
-rw-r--r--app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue84
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js2
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue9
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/components/modal.vue74
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue64
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue204
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/actions.js82
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/getters.js13
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/index.js17
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js7
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/mutations.js79
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/state.js71
-rw-r--r--app/assets/javascripts/reports/grouped_test_report/store/utils.js111
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue2
-rw-r--r--app/assets/javascripts/repository/constants.js4
-rw-r--r--app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue58
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue49
-rw-r--r--app/assets/javascripts/search/sidebar/components/results_filters.vue49
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_navigation.vue66
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js11
-rw-r--r--app/assets/javascripts/search/store/actions.js18
-rw-r--r--app/assets/javascripts/search/store/index.js4
-rw-r--r--app/assets/javascripts/search/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/search/store/mutations.js4
-rw-r--r--app/assets/javascripts/search/store/state.js3
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue16
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js4
-rw-r--r--app/assets/javascripts/sentry/constants.js43
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js45
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue115
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue35
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue34
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown.vue252
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue185
-rw-r--r--app/assets/javascripts/sidebar/constants.js11
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js344
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js4
-rw-r--r--app/assets/javascripts/token_access/components/token_access.vue6
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue21
-rw-r--r--app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql4
-rw-r--r--app/assets/javascripts/tracking/tracker.js6
-rw-r--r--app/assets/javascripts/users_select/constants.js11
-rw-r--r--app/assets/javascripts/users_select/index.js214
-rw-r--r--app/assets/javascripts/users_select/utils.js14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue53
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue54
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue26
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue155
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue44
-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/status_icon.vue36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue94
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue83
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue90
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js21
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue7
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/index.js12
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/router.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/gitlab_version_check.vue89
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/group_select.vue195
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue26
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js54
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js32
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue212
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue90
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js49
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js25
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js64
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js32
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue11
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue8
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js2
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_app.vue92
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_mask_item.vue46
-rw-r--r--app/assets/javascripts/webhooks/components/push_events.vue112
-rw-r--r--app/assets/javascripts/webhooks/constants.js19
-rw-r--r--app/assets/javascripts/webhooks/index.js2
-rw-r--r--app/assets/javascripts/webhooks/webhook.js23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue203
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue117
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue91
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue22
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue56
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue192
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue37
-rw-r--r--app/assets/javascripts/work_items/constants.js5
-rw-r--r--app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/milestone.fragment.graphql5
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql19
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql11
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql20
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql17
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql7
-rw-r--r--app/assets/javascripts/work_items/index.js9
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue46
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue8
-rw-r--r--app/assets/javascripts/work_items/router/index.js2
-rw-r--r--app/assets/javascripts/work_items/router/routes.js35
-rw-r--r--app/assets/javascripts/work_items/utils.js6
-rw-r--r--app/assets/javascripts/work_items_hierarchy/components/app.vue2
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss3
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/selects.scss16
-rw-r--r--app/assets/stylesheets/framework/snippets.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss95
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss8
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss10
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss10
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss12
-rw-r--r--app/assets/stylesheets/highlight/themes/white.scss11
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss29
-rw-r--r--app/assets/stylesheets/page_bundles/branches.scss (renamed from app/assets/stylesheets/pages/branches.scss)2
-rw-r--r--app/assets/stylesheets/page_bundles/clusters.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/dashboard.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/design_management.scss215
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss214
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss74
-rw-r--r--app/assets/stylesheets/page_bundles/notifications.scss (renamed from app/assets/stylesheets/pages/notifications.scss)2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/issues.scss10
-rw-r--r--app/assets/stylesheets/pages/ml_experiment_tracking.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss181
-rw-r--r--app/assets/stylesheets/pages/search.scss7
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss276
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss142
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss94
-rw-r--r--app/assets/stylesheets/themes/_dark.scss28
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss10
593 files changed, 10020 insertions, 7607 deletions
diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
index ce5342ad1ea..d24285af5c3 100644
--- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
+++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
@@ -104,18 +104,13 @@ export default {
@[$options.EVENT_ERROR]="onError"
@[$options.EVENT_SUCCESS]="onSuccess"
>
- <div ref="container">
+ <div ref="container" data-testid="access-token-section" data-qa-selector="access_token_section">
<template v-if="newToken">
- <!--
- After issue https://gitlab.com/gitlab-org/gitlab/-/issues/360921 is
- closed remove the `initial-visibility`.
- -->
<input-copy-toggle-visibility
:copy-button-title="copyButtonTitle"
:label="label"
:label-for="$options.tokenInputId"
:value="newToken"
- initial-visibility
:form-input-group-props="formInputGroupProps"
>
<template #description>
diff --git a/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js b/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js
index 79d7ff0451a..8c01a3bc607 100644
--- a/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js
+++ b/app/assets/javascripts/admin/application_settings/runner_token_expiration/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { parseInterval } from '~/runner/utils';
+import { parseInterval } from '~/ci/runner/utils';
import ExpirationIntervals from './components/expiration_intervals.vue';
const initRunnerTokenExpirationIntervals = (selector = '#js-runner-token-expiration-intervals') => {
diff --git a/app/assets/javascripts/admin/users/components/actions/delete.vue b/app/assets/javascripts/admin/users/components/actions/delete.vue
index ae0c6731271..d4f9ff4e529 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete.vue
@@ -12,6 +12,10 @@ export default {
type: String,
required: true,
},
+ userId: {
+ type: Number,
+ required: true,
+ },
paths: {
type: Object,
required: true,
diff --git a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
index a39df1cbfb6..413804c9a3b 100644
--- a/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
+++ b/app/assets/javascripts/admin/users/components/actions/delete_with_contributions.vue
@@ -1,17 +1,26 @@
<script>
-import { GlDropdownItem } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { associationsCount } from '~/api/user_api';
import eventHub, { EVENT_OPEN_DELETE_USER_MODAL } from '../modals/delete_user_modal_event_hub';
export default {
+ i18n: {
+ loading: __('Loading'),
+ },
components: {
GlDropdownItem,
+ GlLoadingIcon,
},
props: {
username: {
type: String,
required: true,
},
+ userId: {
+ type: Number,
+ required: true,
+ },
paths: {
type: Object,
required: true,
@@ -22,21 +31,38 @@ export default {
default: () => [],
},
},
+ data() {
+ return {
+ loading: false,
+ };
+ },
methods: {
- onClick() {
+ async onClick() {
+ this.loading = true;
+ try {
+ const { data: associationsCountData } = await associationsCount(this.userId);
+ this.openModal(associationsCountData);
+ } catch (error) {
+ this.openModal(new Error());
+ } finally {
+ this.loading = false;
+ }
+ },
+ openModal(associationsCountData) {
const { username, paths, userDeletionObstacles } = this;
eventHub.$emit(EVENT_OPEN_DELETE_USER_MODAL, {
username,
blockPath: paths.block,
deletePath: paths.deleteWithContributions,
userDeletionObstacles,
+ associationsCount: associationsCountData,
i18n: {
title: s__('AdminUsers|Delete User %{username} and contributions?'),
primaryButtonLabel: s__('AdminUsers|Delete user and contributions'),
- messageBody: s__(`AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues,
- merge requests, and groups linked to them. To avoid data loss,
- consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd},
- it cannot be undone or recovered.`),
+ messageBody: s__(`AdminUsers|You are about to permanently delete the user %{username}. This will delete all issues,
+ merge requests, groups, and projects linked to them. To avoid data loss,
+ consider using the %{strongStart}Block user%{strongEnd} feature instead. After you %{strongStart}Delete user%{strongEnd},
+ you cannot undo this action or recover the data.`),
},
});
},
@@ -45,8 +71,12 @@ export default {
</script>
<template>
- <gl-dropdown-item @click="onClick">
- <span class="gl-text-red-500">
+ <gl-dropdown-item :disabled="loading" :aria-busy="loading" @click.capture.native.stop="onClick">
+ <div v-if="loading" class="gl-display-flex gl-align-items-center">
+ <gl-loading-icon class="gl-mr-3" />
+ {{ $options.i18n.loading }}
+ </div>
+ <span v-else class="gl-text-red-500">
<slot></slot>
</span>
</gl-dropdown-item>
diff --git a/app/assets/javascripts/admin/users/components/associations/associations_list.vue b/app/assets/javascripts/admin/users/components/associations/associations_list.vue
new file mode 100644
index 00000000000..12f98a02809
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/associations/associations_list.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import AssociationsListItem from './associations_list_item.vue';
+
+export default {
+ i18n: {
+ errorMessage: s__(
+ "AdminUsers|An error occurred while fetching this user's contributions, and the request cannot return the number of issues, merge requests, groups, and projects linked to this user. If you proceed with deleting the user, all their contributions will still be deleted.",
+ ),
+ },
+ components: {
+ AssociationsListItem,
+ GlAlert,
+ },
+ props: {
+ associationsCount: {
+ type: [Object, Error],
+ required: true,
+ },
+ },
+ computed: {
+ hasError() {
+ return this.associationsCount instanceof Error;
+ },
+ hasAssociations() {
+ return Object.values(this.associationsCount).some((count) => count > 0);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert v-if="hasError" class="gl-mb-5" variant="danger" :dismissible="false">{{
+ $options.i18n.errorMessage
+ }}</gl-alert>
+ <ul v-else-if="hasAssociations" class="gl-mb-5">
+ <associations-list-item
+ v-if="associationsCount.groups_count"
+ :message="n__('%{count} group', '%{count} groups', associationsCount.groups_count)"
+ :count="associationsCount.groups_count"
+ />
+ <associations-list-item
+ v-if="associationsCount.projects_count"
+ :message="n__('%{count} project', '%{count} projects', associationsCount.projects_count)"
+ :count="associationsCount.projects_count"
+ />
+ <associations-list-item
+ v-if="associationsCount.issues_count"
+ :message="n__('%{count} issue', '%{count} issues', associationsCount.issues_count)"
+ :count="associationsCount.issues_count"
+ />
+ <associations-list-item
+ v-if="associationsCount.merge_requests_count"
+ :message="
+ n__(
+ '%{count} merge request',
+ '%{count} merge requests',
+ associationsCount.merge_requests_count,
+ )
+ "
+ :count="associationsCount.merge_requests_count"
+ />
+ </ul>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/associations/associations_list_item.vue b/app/assets/javascripts/admin/users/components/associations/associations_list_item.vue
new file mode 100644
index 00000000000..88cb24aaf8f
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/associations/associations_list_item.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+
+export default {
+ components: { GlSprintf },
+ props: {
+ message: {
+ type: String,
+ required: true,
+ },
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <gl-sprintf :message="message">
+ <template #count>
+ <strong>{{ count }}</strong>
+ </template>
+ </gl-sprintf>
+ </li>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
index 31fe86775ee..7f02d6dd5b1 100644
--- a/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
+++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
@@ -2,6 +2,7 @@
import { GlModal, GlButton, GlFormInput, GlSprintf } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import AssociationsList from '../associations/associations_list.vue';
import eventHub, { EVENT_OPEN_DELETE_USER_MODAL } from './delete_user_modal_event_hub';
export default {
@@ -11,6 +12,7 @@ export default {
GlFormInput,
GlSprintf,
UserDeletionObstaclesList,
+ AssociationsList,
},
props: {
csrfToken: {
@@ -25,6 +27,7 @@ export default {
blockPath: '',
deletePath: '',
userDeletionObstacles: [],
+ associationsCount: {},
i18n: {
title: '',
primaryButtonLabel: '',
@@ -53,11 +56,19 @@ export default {
eventHub.$off(EVENT_OPEN_DELETE_USER_MODAL, this.onOpenEvent);
},
methods: {
- onOpenEvent({ username, blockPath, deletePath, userDeletionObstacles, i18n }) {
+ onOpenEvent({
+ username,
+ blockPath,
+ deletePath,
+ userDeletionObstacles,
+ associationsCount = {},
+ i18n,
+ }) {
this.username = username;
this.blockPath = blockPath;
this.deletePath = deletePath;
this.userDeletionObstacles = userDeletionObstacles;
+ this.associationsCount = associationsCount;
this.i18n = i18n;
this.openModal();
},
@@ -100,8 +111,10 @@ export default {
:user-name="trimmedUsername"
/>
+ <associations-list :associations-count="associationsCount" />
+
<p>
- <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
+ <gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}.')">
<template #username>
<code data-testid="confirm-username" class="gl-white-space-pre-wrap">{{
trimmedUsername
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index 691a292673c..c1fb80959cf 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -139,6 +139,7 @@ export default {
:key="action"
:paths="userPaths"
:username="user.name"
+ :user-id="user.id"
:user-deletion-obstacles="obstaclesForUserDeletion"
:data-testid="`delete-${action}`"
>
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 6b5aac57f1c..03bc4b825ae 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -612,7 +612,7 @@ export default {
>
<alert-settings-form-help-block
:message="viewCredentialsHelpMsg"
- link="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
+ link="https://docs.gitlab.com/ee/operations/incident_management/integrations.html#configuration"
/>
<gl-form-group id="integration-webhook">
diff --git a/app/assets/javascripts/analytics/shared/components/daterange.vue b/app/assets/javascripts/analytics/shared/components/daterange.vue
index 92ccac59057..a14b0bafecf 100644
--- a/app/assets/javascripts/analytics/shared/components/daterange.vue
+++ b/app/assets/javascripts/analytics/shared/components/daterange.vue
@@ -49,7 +49,7 @@ export default {
return {
maxDateRangeTooltip: sprintf(
__(
- 'Showing data for workflow items created in this date range. Date range limited to %{maxDateRange} days.',
+ 'Showing data for workflow items completed in this date range. Date range limited to %{maxDateRange} days.',
),
{
maxDateRange: this.maxDateRange,
@@ -89,6 +89,8 @@ export default {
:default-max-date="maxDate"
:same-day-selection="includeSelectedDate"
:tooltip="maxDateRangeTooltip"
+ :from-label="__('From')"
+ :to-label="__('To')"
theme="animate-picker"
start-picker-class="js-daterange-picker-from gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-lg-align-items-center gl-lg-mr-3 gl-mb-2 gl-lg-mb-0"
end-picker-class="js-daterange-picker-to gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-lg-align-items-center gl-mb-2 gl-lg-mb-0"
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 b2e554bc913..5bb60d91f1e 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -240,7 +240,7 @@ export default {
</template>
<template #header>
<gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
- <gl-search-box-by-type v-model.trim="searchTerm" />
+ <gl-search-box-by-type v-model.trim="searchTerm" :placeholder="__('Search')" />
</template>
<template #highlighted-items>
<gl-dropdown-item
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index ffb61230661..cc7b554f32c 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -1,33 +1,11 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
-import { flatten, isEqual, keyBy } from 'lodash';
+import { isEqual, keyBy } from 'lodash';
import { createAlert } from '~/flash';
import { sprintf, s__ } from '~/locale';
-import { METRICS_POPOVER_CONTENT } from '../constants';
-import { removeFlash, prepareTimeMetricsData } from '../utils';
+import { fetchMetricsData, removeFlash } from '../utils';
import MetricTile from './metric_tile.vue';
-const requestData = ({ request, endpoint, path, params, name }) => {
- return request({ endpoint, params, requestPath: path })
- .then(({ data }) => data)
- .catch(() => {
- const message = sprintf(
- s__(
- 'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
- ),
- { requestTypeName: name },
- );
- createAlert({ message });
- });
-};
-
-const fetchMetricsData = (reqs = [], path, params) => {
- const promises = reqs.map((r) => requestData({ ...r, path, params }));
- return Promise.all(promises).then((responses) =>
- prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT),
- );
-};
-
const extractMetricsGroupData = (keyList = [], data = []) => {
if (!keyList.length || !data.length) return [];
const kv = keyBy(data, 'identifier');
@@ -111,7 +89,15 @@ export default {
this.isLoading = false;
})
- .catch(() => {
+ .catch((err) => {
+ const message = sprintf(
+ s__(
+ 'ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data.',
+ ),
+ { requestTypeName: err.message },
+ );
+
+ createAlert({ message });
this.isLoading = false;
});
},
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
index bc52e38fc81..71b719d1ed2 100644
--- a/app/assets/javascripts/analytics/shared/utils.js
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -1,8 +1,9 @@
+import { flatten } from 'lodash';
import { hideFlash } from '~/flash';
import dateFormat from '~/lib/dateformat';
import { slugify } from '~/lib/utils/text_utility';
import { urlQueryToFilter } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import { dateFormats } from './constants';
+import { dateFormats, METRICS_POPOVER_CONTENT } from './constants';
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
if (!searchTerm?.length) return data;
@@ -96,3 +97,28 @@ export const prepareTimeMetricsData = (data = [], popoverContent = {}) =>
description: popoverContent[metricIdentifier]?.description || '',
};
});
+
+const requestData = ({ request, endpoint, requestPath, params, name }) => {
+ return request({ endpoint, params, requestPath })
+ .then(({ data }) => data)
+ .catch(() => {
+ throw new Error(name);
+ });
+};
+
+/**
+ * Takes a configuration array of metrics requests (key metrics and DORA) and returns
+ * a flat array of all the responses. Different metrics are retrieved from different endpoints
+ * additionally we only support certain metrics for FOSS users.
+ *
+ * @param {Array} requests - array of metric api requests to be made
+ * @param {String} requestPath - path for the group / project we are requesting
+ * @param {Object} params - optional parameters to filter, including `created_after` and `created_before` dates
+ * @returns a flat array of metrics
+ */
+export const fetchMetricsData = (requests = [], requestPath, params) => {
+ const promises = requests.map((r) => requestData({ ...r, requestPath, params }));
+ return Promise.all(promises).then((responses) =>
+ prepareTimeMetricsData(flatten(responses), METRICS_POPOVER_CONTENT),
+ );
+};
diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js
index 48cf346d0e6..e859160c2e7 100644
--- a/app/assets/javascripts/api/groups_api.js
+++ b/app/assets/javascripts/api/groups_api.js
@@ -5,6 +5,7 @@ import { buildApiUrl } from './api_utils';
const GROUP_PATH = '/api/:version/groups/:id';
const GROUPS_PATH = '/api/:version/groups.json';
const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups';
+const GROUP_TRANSFER_LOCATIONS_PATH = 'api/:version/groups/:id/transfer_locations';
const axiosGet = (url, query, options, callback) => {
return axios
@@ -37,3 +38,10 @@ export function updateGroup(groupId, data = {}) {
return axios.put(url, data);
}
+
+export const getGroupTransferLocations = (groupId, params = {}) => {
+ const url = buildApiUrl(GROUP_TRANSFER_LOCATIONS_PATH).replace(':id', groupId);
+ const defaultParams = { per_page: DEFAULT_PER_PAGE };
+
+ return axios.get(url, { params: { ...defaultParams, ...params } });
+};
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index 369abe95d49..0f874e35684 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -12,6 +12,7 @@ const USER_PROJECTS_PATH = '/api/:version/users/:id/projects';
const USER_POST_STATUS_PATH = '/api/:version/user/status';
const USER_FOLLOW_PATH = '/api/:version/users/:id/follow';
const USER_UNFOLLOW_PATH = '/api/:version/users/:id/unfollow';
+const USER_ASSOCIATIONS_COUNT_PATH = '/api/:version/users/:id/associations_count';
export function getUsers(query, options) {
const url = buildApiUrl(USERS_PATH);
@@ -81,3 +82,8 @@ export function unfollowUser(userId) {
const url = buildApiUrl(USER_UNFOLLOW_PATH).replace(':id', encodeURIComponent(userId));
return axios.post(url);
}
+
+export function associationsCount(userId) {
+ const url = buildApiUrl(USER_ASSOCIATIONS_COUNT_PATH).replace(':id', encodeURIComponent(userId));
+ return axios.get(url);
+}
diff --git a/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue b/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue
new file mode 100644
index 00000000000..14edd73824e
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifact_delete_modal.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+
+import {
+ I18N_MODAL_TITLE,
+ I18N_MODAL_BODY,
+ I18N_MODAL_PRIMARY,
+ I18N_MODAL_CANCEL,
+} from '../constants';
+
+export default {
+ components: {
+ GlModal,
+ },
+ props: {
+ artifactName: {
+ type: String,
+ required: true,
+ },
+ deleteInProgress: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ actionPrimary() {
+ return {
+ text: I18N_MODAL_PRIMARY,
+ attributes: { variant: 'danger', loading: this.deleteInProgress },
+ };
+ },
+ },
+ actionCancel: { text: I18N_MODAL_CANCEL },
+ i18n: {
+ title: I18N_MODAL_TITLE,
+ body: I18N_MODAL_BODY,
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ modal-id="artifact-delete-modal"
+ size="sm"
+ :title="$options.i18n.title(artifactName)"
+ :action-primary="actionPrimary"
+ :action-cancel="$options.actionCancel"
+ v-bind="$attrs"
+ v-on="$listeners"
+ >
+ {{ $options.i18n.body }}
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/artifact_row.vue b/app/assets/javascripts/artifacts/components/artifact_row.vue
new file mode 100644
index 00000000000..8c03db2acd1
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifact_row.vue
@@ -0,0 +1,87 @@
+<script>
+import { GlButtonGroup, GlButton, GlBadge, GlFriendlyWrap } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { I18N_EXPIRED, I18N_DOWNLOAD, I18N_DELETE } from '../constants';
+
+export default {
+ name: 'ArtifactRow',
+ components: {
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlFriendlyWrap,
+ },
+ props: {
+ artifact: {
+ type: Object,
+ required: true,
+ },
+ isLastRow: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ isExpired() {
+ if (!this.artifact.expireAt) {
+ return false;
+ }
+ return Date.now() > new Date(this.artifact.expireAt).getTime();
+ },
+ artifactSize() {
+ return numberToHumanSize(this.artifact.size);
+ },
+ },
+ i18n: {
+ expired: I18N_EXPIRED,
+ download: I18N_DOWNLOAD,
+ delete: I18N_DELETE,
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-py-4"
+ :class="{ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': !isLastRow }"
+ >
+ <div class="gl-display-inline-flex gl-align-items-center gl-w-full">
+ <span
+ class="gl-w-half gl-pl-8 gl-display-flex gl-align-items-center"
+ data-testid="job-artifact-row-name"
+ >
+ <gl-friendly-wrap :text="artifact.name" />
+ <gl-badge size="sm" variant="neutral" class="gl-ml-2">
+ {{ artifact.fileType.toLowerCase() }}
+ </gl-badge>
+ <gl-badge v-if="isExpired" size="sm" variant="warning" icon="expire" class="gl-ml-2">
+ {{ $options.i18n.expired }}
+ </gl-badge>
+ </span>
+
+ <span class="gl-w-quarter gl-text-right gl-pr-5" data-testid="job-artifact-row-size">
+ {{ artifactSize }}
+ </span>
+
+ <span class="gl-w-quarter gl-text-right gl-pr-5">
+ <gl-button-group>
+ <gl-button
+ category="tertiary"
+ icon="download"
+ :title="$options.i18n.download"
+ :aria-label="$options.i18n.download"
+ :href="artifact.downloadPath"
+ data-testid="job-artifact-row-download-button"
+ />
+ <gl-button
+ category="tertiary"
+ icon="remove"
+ :title="$options.i18n.delete"
+ :aria-label="$options.i18n.delete"
+ data-testid="job-artifact-row-delete-button"
+ @click="$emit('delete')"
+ />
+ </gl-button-group>
+ </span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
new file mode 100644
index 00000000000..4a826d0d462
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/artifacts_table_row_details.vue
@@ -0,0 +1,118 @@
+<script>
+import { createAlert } from '~/flash';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
+import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
+import destroyArtifactMutation from '../graphql/mutations/destroy_artifact.mutation.graphql';
+import { removeArtifactFromStore } from '../graphql/cache_update';
+import {
+ I18N_DESTROY_ERROR,
+ ARTIFACT_ROW_HEIGHT,
+ ARTIFACTS_SHOWN_WITHOUT_SCROLLING,
+} from '../constants';
+import ArtifactRow from './artifact_row.vue';
+import ArtifactDeleteModal from './artifact_delete_modal.vue';
+
+export default {
+ name: 'ArtifactsTableRowDetails',
+ components: {
+ DynamicScroller,
+ DynamicScrollerItem,
+ ArtifactRow,
+ ArtifactDeleteModal,
+ },
+ props: {
+ artifacts: {
+ type: Object,
+ required: true,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isModalVisible: false,
+ deleteInProgress: false,
+ deletingArtifactId: null,
+ deletingArtifactName: '',
+ };
+ },
+ computed: {
+ scrollContainerStyle() {
+ /*
+ limit the height of the expanded artifacts container to a number of artifacts
+ if a job has more artifacts than ARTIFACTS_SHOWN_WITHOUT_SCROLLING, scroll to see the rest
+ add one pixel to row height to account for borders
+ */
+ return { maxHeight: `${ARTIFACTS_SHOWN_WITHOUT_SCROLLING * (ARTIFACT_ROW_HEIGHT + 1)}px` };
+ },
+ },
+ methods: {
+ isLastRow(index) {
+ return index === this.artifacts.nodes.length - 1;
+ },
+ showModal(item) {
+ this.deletingArtifactId = item.id;
+ this.deletingArtifactName = item.name;
+ this.isModalVisible = true;
+ },
+ hideModal() {
+ this.isModalVisible = false;
+ },
+ clearModal() {
+ this.deletingArtifactId = null;
+ this.deletingArtifactName = '';
+ },
+ destroyArtifact() {
+ const id = this.deletingArtifactId;
+ this.deleteInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: destroyArtifactMutation,
+ variables: { id },
+ update: (store) => {
+ removeArtifactFromStore(store, id, getJobArtifactsQuery, this.queryVariables);
+ },
+ })
+ .catch(() => {
+ createAlert({
+ message: I18N_DESTROY_ERROR,
+ });
+ this.$emit('refetch');
+ })
+ .finally(() => {
+ this.deleteInProgress = false;
+ this.clearModal();
+ });
+ },
+ },
+ ARTIFACT_ROW_HEIGHT,
+};
+</script>
+<template>
+ <div :style="scrollContainerStyle">
+ <dynamic-scroller :items="artifacts.nodes" :min-item-size="$options.ARTIFACT_ROW_HEIGHT">
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
+ <artifact-row
+ :artifact="item"
+ :is-last-row="isLastRow(index)"
+ @delete="showModal(item)"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
+ <artifact-delete-modal
+ :artifact-name="deletingArtifactName"
+ :visible="isModalVisible"
+ :delete-in-progress="deleteInProgress"
+ @primary="destroyArtifact"
+ @cancel="hideModal"
+ @close="hideModal"
+ @hide="hideModal"
+ @hidden="clearModal"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
new file mode 100644
index 00000000000..34e443f4e58
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
@@ -0,0 +1,337 @@
+<script>
+import {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlPagination,
+} from '@gitlab/ui';
+import { createAlert } from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql';
+import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils';
+import {
+ STATUS_BADGE_VARIANTS,
+ I18N_DOWNLOAD,
+ I18N_BROWSE,
+ I18N_DELETE,
+ I18N_EXPIRED,
+ I18N_DESTROY_ERROR,
+ I18N_FETCH_ERROR,
+ I18N_ARTIFACTS,
+ I18N_JOB,
+ I18N_SIZE,
+ I18N_CREATED,
+ I18N_ARTIFACTS_COUNT,
+ INITIAL_CURRENT_PAGE,
+ INITIAL_PREVIOUS_PAGE_CURSOR,
+ INITIAL_NEXT_PAGE_CURSOR,
+ JOBS_PER_PAGE,
+ INITIAL_LAST_PAGE_SIZE,
+} from '../constants';
+import ArtifactsTableRowDetails from './artifacts_table_row_details.vue';
+
+const INITIAL_PAGINATION_STATE = {
+ currentPage: INITIAL_CURRENT_PAGE,
+ prevPageCursor: INITIAL_PREVIOUS_PAGE_CURSOR,
+ nextPageCursor: INITIAL_NEXT_PAGE_CURSOR,
+ firstPageSize: JOBS_PER_PAGE,
+ lastPageSize: INITIAL_LAST_PAGE_SIZE,
+};
+
+export default {
+ name: 'JobArtifactsTable',
+ components: {
+ GlLoadingIcon,
+ GlTable,
+ GlLink,
+ GlButtonGroup,
+ GlButton,
+ GlBadge,
+ GlIcon,
+ GlPagination,
+ CiIcon,
+ TimeAgo,
+ ArtifactsTableRowDetails,
+ },
+ inject: ['projectPath'],
+ apollo: {
+ jobArtifacts: {
+ query: getJobArtifactsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update({ project: { jobs: { nodes = [], pageInfo = {}, count = 0 } = {} } }) {
+ this.pageInfo = pageInfo;
+ this.count = count;
+ return nodes
+ .map(mapArchivesToJobNodes)
+ .map(mapBooleansToJobNodes)
+ .map((jobNode) => {
+ return {
+ ...jobNode,
+ // GlTable uses an item's _showDetails attribute to determine whether
+ // it should show the <template #row-details /> for its table row
+ _showDetails: this.expandedJobs.includes(jobNode.id),
+ };
+ });
+ },
+ error() {
+ createAlert({
+ message: I18N_FETCH_ERROR,
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ jobArtifacts: [],
+ count: 0,
+ pageInfo: {},
+ expandedJobs: [],
+ pagination: INITIAL_PAGINATION_STATE,
+ };
+ },
+ computed: {
+ queryVariables() {
+ return {
+ projectPath: this.projectPath,
+ firstPageSize: this.pagination.firstPageSize,
+ lastPageSize: this.pagination.lastPageSize,
+ prevPageCursor: this.pagination.prevPageCursor,
+ nextPageCursor: this.pagination.nextPageCursor,
+ };
+ },
+ showPagination() {
+ return this.count > JOBS_PER_PAGE;
+ },
+ prevPage() {
+ return Number(this.pageInfo.hasPreviousPage);
+ },
+ nextPage() {
+ return Number(this.pageInfo.hasNextPage);
+ },
+ },
+ methods: {
+ refetchArtifacts() {
+ this.$apollo.queries.jobArtifacts.refetch();
+ },
+ artifactsSize(item) {
+ return totalArtifactsSizeForJob(item);
+ },
+ pipelineId(item) {
+ const id = getIdFromGraphQLId(item.pipeline.id);
+ return `#${id}`;
+ },
+ handlePageChange(page) {
+ const { startCursor, endCursor } = this.pageInfo;
+
+ if (page > this.pagination.currentPage) {
+ this.pagination = {
+ ...INITIAL_PAGINATION_STATE,
+ nextPageCursor: endCursor,
+ currentPage: page,
+ };
+ } else {
+ this.pagination = {
+ lastPageSize: JOBS_PER_PAGE,
+ firstPageSize: null,
+ prevPageCursor: startCursor,
+ currentPage: page,
+ };
+ }
+ },
+ handleRowToggle(toggleDetails, hasArtifacts, id, detailsShowing) {
+ if (!hasArtifacts) return;
+ toggleDetails();
+
+ if (!detailsShowing) {
+ this.expandedJobs.push(id);
+ } else {
+ this.expandedJobs.splice(this.expandedJobs.indexOf(id), 1);
+ }
+ },
+ downloadPath(job) {
+ return job.archive?.downloadPath;
+ },
+ downloadButtonDisabled(job) {
+ return !job.archive?.downloadPath;
+ },
+ browseButtonDisabled(job) {
+ return !job.browseArtifactsPath;
+ },
+ },
+ fields: [
+ {
+ key: 'artifacts',
+ label: I18N_ARTIFACTS,
+ thClass: 'gl-w-quarter',
+ },
+ {
+ key: 'job',
+ label: I18N_JOB,
+ thClass: 'gl-w-35p',
+ },
+ {
+ key: 'size',
+ label: I18N_SIZE,
+ thClass: 'gl-w-15p gl-text-right',
+ tdClass: 'gl-text-right',
+ },
+ {
+ key: 'created',
+ label: I18N_CREATED,
+ thClass: 'gl-w-eighth gl-text-center',
+ tdClass: 'gl-text-center',
+ },
+ {
+ key: 'actions',
+ label: '',
+ thClass: 'gl-w-eighth',
+ tdClass: 'gl-text-right',
+ },
+ ],
+ STATUS_BADGE_VARIANTS,
+ i18n: {
+ download: I18N_DOWNLOAD,
+ browse: I18N_BROWSE,
+ delete: I18N_DELETE,
+ expired: I18N_EXPIRED,
+ destroyArtifactError: I18N_DESTROY_ERROR,
+ fetchArtifactsError: I18N_FETCH_ERROR,
+ artifactsLabel: I18N_ARTIFACTS,
+ jobLabel: I18N_JOB,
+ sizeLabel: I18N_SIZE,
+ createdLabel: I18N_CREATED,
+ artifactsCount: I18N_ARTIFACTS_COUNT,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-table
+ :items="jobArtifacts"
+ :fields="$options.fields"
+ :busy="$apollo.queries.jobArtifacts.loading"
+ stacked="sm"
+ details-td-class="gl-bg-gray-10! gl-p-0! gl-overflow-auto"
+ >
+ <template #table-busy>
+ <gl-loading-icon size="lg" />
+ </template>
+ <template
+ #cell(artifacts)="{ item: { id, artifacts, hasArtifacts }, toggleDetails, detailsShowing }"
+ >
+ <span
+ :class="{ 'gl-cursor-pointer': hasArtifacts }"
+ data-testid="job-artifacts-count"
+ @click="handleRowToggle(toggleDetails, hasArtifacts, id, detailsShowing)"
+ >
+ <gl-icon
+ v-if="hasArtifacts"
+ :name="detailsShowing ? 'chevron-down' : 'chevron-right'"
+ class="gl-mr-2"
+ />
+ <strong>
+ {{ $options.i18n.artifactsCount(artifacts.nodes.length) }}
+ </strong>
+ </span>
+ </template>
+ <template #cell(job)="{ item }">
+ <span class="gl-display-inline-flex gl-align-items-center gl-w-full gl-mb-4">
+ <span data-testid="job-artifacts-job-status">
+ <ci-icon v-if="item.succeeded" :status="item.detailedStatus" class="gl-mr-3" />
+ <gl-badge
+ v-else
+ :icon="item.detailedStatus.icon"
+ :variant="$options.STATUS_BADGE_VARIANTS[item.detailedStatus.group]"
+ class="gl-mr-3"
+ >
+ {{ item.detailedStatus.label }}
+ </gl-badge>
+ </span>
+ <gl-link :href="item.webPath" class="gl-font-weight-bold">
+ {{ item.name }}
+ </gl-link>
+ </span>
+ <span class="gl-display-inline-flex">
+ <gl-icon name="pipeline" class="gl-mr-2" />
+ <gl-link
+ :href="item.pipeline.path"
+ class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
+ >
+ {{ pipelineId(item) }}
+ </gl-link>
+ <gl-icon name="branch" class="gl-mr-2" />
+ <gl-link
+ :href="item.refPath"
+ class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
+ >
+ {{ item.refName }}
+ </gl-link>
+ <gl-icon name="commit" class="gl-mr-2" />
+ <gl-link
+ :href="item.commitPath"
+ class="gl-text-black-normal gl-text-decoration-underline gl-mr-4"
+ >
+ {{ item.shortSha }}
+ </gl-link>
+ </span>
+ </template>
+ <template #cell(size)="{ item }">
+ <span data-testid="job-artifacts-size">{{ artifactsSize(item) }}</span>
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago data-testid="job-artifacts-created" :time="item.finishedAt" />
+ </template>
+ <template #cell(actions)="{ item }">
+ <gl-button-group>
+ <gl-button
+ icon="download"
+ :disabled="downloadButtonDisabled(item)"
+ :href="downloadPath(item)"
+ :title="$options.i18n.download"
+ :aria-label="$options.i18n.download"
+ data-testid="job-artifacts-download-button"
+ />
+ <gl-button
+ icon="folder-open"
+ :disabled="browseButtonDisabled(item)"
+ :href="item.browseArtifactsPath"
+ :title="$options.i18n.browse"
+ :aria-label="$options.i18n.browse"
+ data-testid="job-artifacts-browse-button"
+ />
+ <gl-button
+ icon="remove"
+ :title="$options.i18n.delete"
+ :aria-label="$options.i18n.delete"
+ data-testid="job-artifacts-delete-button"
+ disabled
+ />
+ </gl-button-group>
+ </template>
+ <template #row-details="{ item: { artifacts } }">
+ <artifacts-table-row-details
+ :artifacts="artifacts"
+ :query-variables="queryVariables"
+ @refetch="refetchArtifacts"
+ />
+ </template>
+ </gl-table>
+ <gl-pagination
+ v-if="showPagination"
+ :value="pagination.currentPage"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ align="center"
+ class="gl-mt-3"
+ @input="handlePageChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/artifacts/constants.js
new file mode 100644
index 00000000000..5fcc4f2b76e
--- /dev/null
+++ b/app/assets/javascripts/artifacts/constants.js
@@ -0,0 +1,55 @@
+import { __, s__, n__, sprintf } from '~/locale';
+
+export const JOB_STATUS_GROUP_SUCCESS = 'success';
+
+export const STATUS_BADGE_VARIANTS = {
+ success: 'success',
+ passed: 'success',
+ error: 'danger',
+ failed: 'danger',
+ pending: 'warning',
+ 'waiting-for-resource': 'warning',
+ 'failed-with-warnings': 'warning',
+ 'success-with-warnings': 'warning',
+ running: 'info',
+ canceled: 'neutral',
+ disabled: 'neutral',
+ scheduled: 'neutral',
+ manual: 'neutral',
+ notification: 'muted',
+ preparing: 'muted',
+ created: 'muted',
+ skipped: 'muted',
+ notfound: 'muted',
+};
+
+export const I18N_DOWNLOAD = __('Download');
+export const I18N_BROWSE = s__('Artifacts|Browse');
+export const I18N_DELETE = __('Delete');
+export const I18N_EXPIRED = __('Expired');
+export const I18N_DESTROY_ERROR = s__('Artifacts|An error occurred while deleting the artifact');
+export const I18N_FETCH_ERROR = s__('Artifacts|An error occurred while retrieving job artifacts');
+export const I18N_ARTIFACTS = __('Artifacts');
+export const I18N_JOB = __('Job');
+export const I18N_SIZE = __('Size');
+export const I18N_CREATED = __('Created');
+export const I18N_ARTIFACTS_COUNT = (count) => n__('%d file', '%d files', count);
+
+export const I18N_MODAL_TITLE = (artifactName) =>
+ sprintf(s__('Artifacts|Delete %{name}?'), { name: artifactName });
+export const I18N_MODAL_BODY = s__(
+ 'Artifacts|This artifact will be permanently deleted. Any reports generated from this artifact will be empty.',
+);
+export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
+export const I18N_MODAL_CANCEL = __('Cancel');
+
+export const INITIAL_CURRENT_PAGE = 1;
+export const INITIAL_PREVIOUS_PAGE_CURSOR = '';
+export const INITIAL_NEXT_PAGE_CURSOR = '';
+export const JOBS_PER_PAGE = 20;
+export const INITIAL_LAST_PAGE_SIZE = null;
+
+export const ARCHIVE_FILE_TYPE = 'ARCHIVE';
+
+export const ARTIFACT_ROW_HEIGHT = 56;
+export const ARTIFACTS_SHOWN_WITHOUT_SCROLLING = 4;
diff --git a/app/assets/javascripts/artifacts/graphql/cache_update.js b/app/assets/javascripts/artifacts/graphql/cache_update.js
new file mode 100644
index 00000000000..9fa6114c7d4
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/cache_update.js
@@ -0,0 +1,30 @@
+import produce from 'immer';
+
+export const hasErrors = ({ errors = [] }) => errors?.length;
+
+export function removeArtifactFromStore(store, deletedArtifactId, query, variables) {
+ if (hasErrors(deletedArtifactId)) return;
+
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.project.jobs.nodes = draftData.project.jobs.nodes.map((jobNode) => {
+ return {
+ ...jobNode,
+ artifacts: {
+ ...jobNode.artifacts,
+ nodes: jobNode.artifacts.nodes.filter(({ id }) => id !== deletedArtifactId),
+ },
+ };
+ });
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+}
diff --git a/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql b/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
new file mode 100644
index 00000000000..529224b47e6
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/mutations/destroy_artifact.mutation.graphql
@@ -0,0 +1,7 @@
+mutation destroyArtifact($id: CiJobArtifactID!) {
+ artifactDestroy(input: { id: $id }) {
+ artifact {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
new file mode 100644
index 00000000000..89a24d7891e
--- /dev/null
+++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
@@ -0,0 +1,57 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getJobArtifacts(
+ $projectPath: ID!
+ $firstPageSize: Int
+ $lastPageSize: Int
+ $prevPageCursor: String = ""
+ $nextPageCursor: String = ""
+) {
+ project(fullPath: $projectPath) {
+ id
+ jobs(
+ statuses: [SUCCESS, FAILED]
+ first: $firstPageSize
+ last: $lastPageSize
+ after: $nextPageCursor
+ before: $prevPageCursor
+ ) {
+ count
+ nodes {
+ id
+ name
+ webPath
+ detailedStatus {
+ id
+ group
+ icon
+ label
+ }
+ pipeline {
+ id
+ iid
+ path
+ }
+ refName
+ refPath
+ shortSha
+ commitPath
+ finishedAt
+ browseArtifactsPath
+ artifacts {
+ nodes {
+ id
+ name
+ fileType
+ downloadPath
+ size
+ expireAt
+ }
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/artifacts/index.js b/app/assets/javascripts/artifacts/index.js
new file mode 100644
index 00000000000..b5146e0f0e9
--- /dev/null
+++ b/app/assets/javascripts/artifacts/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import JobArtifactsTable from './components/job_artifacts_table.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const initArtifactsTable = () => {
+ const el = document.querySelector('#js-artifact-management');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ },
+ render: (createElement) => createElement(JobArtifactsTable),
+ });
+};
diff --git a/app/assets/javascripts/artifacts/utils.js b/app/assets/javascripts/artifacts/utils.js
new file mode 100644
index 00000000000..ebcf0af8d2a
--- /dev/null
+++ b/app/assets/javascripts/artifacts/utils.js
@@ -0,0 +1,26 @@
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { ARCHIVE_FILE_TYPE, JOB_STATUS_GROUP_SUCCESS } from './constants';
+
+export const totalArtifactsSizeForJob = (job) =>
+ numberToHumanSize(
+ job.artifacts.nodes
+ .map((artifact) => artifact.size)
+ .reduce((total, artifact) => total + artifact, 0),
+ );
+
+export const mapArchivesToJobNodes = (jobNode) => {
+ return {
+ archive: {
+ ...jobNode.artifacts.nodes.find((artifact) => artifact.fileType === ARCHIVE_FILE_TYPE),
+ },
+ ...jobNode,
+ };
+};
+
+export const mapBooleansToJobNodes = (jobNode) => {
+ return {
+ succeeded: jobNode.detailedStatus.group === JOB_STATUS_GROUP_SUCCESS,
+ hasArtifacts: jobNode.artifacts.nodes.length > 0,
+ ...jobNode,
+ };
+};
diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js
index a653769b60f..ae186aba32d 100644
--- a/app/assets/javascripts/behaviors/copy_code.js
+++ b/app/assets/javascripts/behaviors/copy_code.js
@@ -53,9 +53,10 @@ export const initCopyCodeButton = (selector = '#content-body') => {
customElements.define('copy-code', CopyCodeButton);
}
+ const exclude = document.querySelector('.file-content.code'); // this behavior is not needed when viewing raw file content, so excluding it as the unnecessary dom lookups can become expensive
const el = document.querySelector(selector);
- if (!el) return () => {};
+ if (!el || exclude) return () => {};
const observer = new MutationObserver(() => addCodeButton());
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index ee5c0fe5ef3..a08cf48c327 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -15,7 +15,7 @@ $.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight').get());
renderKroki(this.find('.js-render-kroki[hidden]').get());
renderMath(this.find('.js-render-math'));
- renderSandboxedMermaid(this.find('.js-render-mermaid'));
+ renderSandboxedMermaid(this.find('.js-render-mermaid').get());
renderJSONTable(
Array.from(this.find('[lang="json"][data-lang-params="table"]').get()).map((e) => e.parentNode),
);
diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
index 077e96b2fee..66007aa9e3d 100644
--- a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
@@ -1,5 +1,4 @@
-import $ from 'jquery';
-import { once, countBy } from 'lodash';
+import { countBy } from 'lodash';
import { __ } from '~/locale';
import {
getBaseURL,
@@ -8,7 +7,8 @@ import {
joinPaths,
} from '~/lib/utils/url_utility';
import { darkModeEnabled } from '~/lib/utils/color_utils';
-import { setAttributes } from '~/lib/utils/dom_utils';
+import { setAttributes, isElementVisible } from '~/lib/utils/dom_utils';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { unrestrictedPages } from './constants';
// Renders diagrams and flowcharts from text using Mermaid in any element with the
@@ -27,17 +27,30 @@ import { unrestrictedPages } from './constants';
const SANDBOX_FRAME_PATH = '/-/sandbox/mermaid';
// This is an arbitrary number; Can be iterated upon when suitable.
-const MAX_CHAR_LIMIT = 2000;
+export const MAX_CHAR_LIMIT = 2000;
// Max # of mermaid blocks that can be rendered in a page.
-const MAX_MERMAID_BLOCK_LIMIT = 50;
+export const MAX_MERMAID_BLOCK_LIMIT = 50;
// Max # of `&` allowed in Chaining of links syntax
const MAX_CHAINING_OF_LINKS_LIMIT = 30;
+
export const BUFFER_IFRAME_HEIGHT = 10;
export const SANDBOX_ATTRIBUTES = 'allow-scripts allow-popups';
+
+const ALERT_CONTAINER_CLASS = 'mermaid-alert-container';
+export const LAZY_ALERT_SHOWN_CLASS = 'lazy-alert-shown';
+
// Keep a map of mermaid blocks we've already rendered.
const elsProcessingMap = new WeakMap();
let renderedMermaidBlocks = 0;
+/**
+ * Determines whether a given Mermaid diagram is visible.
+ *
+ * @param {Element} el The Mermaid DOM node
+ * @returns
+ */
+const isVisibleMermaid = (el) => el.closest('details') === null && isElementVisible(el);
+
function shouldLazyLoadMermaidBlock(source) {
/**
* If source contains `&`, which means that it might
@@ -104,8 +117,8 @@ function renderMermaidEl(el, source) {
);
}
-function renderMermaids($els) {
- if (!$els.length) return;
+function renderMermaids(els) {
+ if (!els.length) return;
const pageName = document.querySelector('body').dataset.page;
@@ -114,7 +127,7 @@ function renderMermaids($els) {
let renderedChars = 0;
- $els.each((i, el) => {
+ els.forEach((el) => {
// Skipping all the elements which we've already queued in requestIdleCallback
if (elsProcessingMap.has(el)) {
return;
@@ -133,33 +146,29 @@ function renderMermaids($els) {
renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
shouldLazyLoadMermaidBlock(source))
) {
- const html = `
- <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
- <div>
- <div>
- <div class="js-warning-text"></div>
- <div class="gl-alert-actions">
- <button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-confirm btn-md gl-button">Display</button>
- </div>
- </div>
- <button type="button" class="close" data-dismiss="alert" aria-label="Close">
- <span aria-hidden="true">&times;</span>
- </button>
- </div>
- </div>
- `;
-
- const $parent = $(el).parent();
-
- if (!$parent.hasClass('lazy-alert-shown')) {
- $parent.after(html);
- $parent
- .siblings()
- .find('.js-warning-text')
- .text(
- __('Warning: Displaying this diagram might cause performance issues on this page.'),
- );
- $parent.addClass('lazy-alert-shown');
+ const parent = el.parentNode;
+
+ if (!parent.classList.contains(LAZY_ALERT_SHOWN_CLASS)) {
+ const alertContainer = document.createElement('div');
+ alertContainer.classList.add(ALERT_CONTAINER_CLASS);
+ alertContainer.classList.add('gl-mb-5');
+ parent.after(alertContainer);
+ createAlert({
+ message: __(
+ 'Warning: Displaying this diagram might cause performance issues on this page.',
+ ),
+ variant: VARIANT_WARNING,
+ parent: parent.parentNode,
+ containerSelector: `.${ALERT_CONTAINER_CLASS}`,
+ primaryButton: {
+ text: __('Display'),
+ clickHandler: () => {
+ alertContainer.remove();
+ renderMermaidEl(el, source);
+ },
+ },
+ });
+ parent.classList.add(LAZY_ALERT_SHOWN_CLASS);
}
return;
@@ -176,37 +185,33 @@ function renderMermaids($els) {
});
}
-const hookLazyRenderMermaidEvent = once(() => {
- $(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
- const parent = $(this).closest('.js-lazy-render-mermaid-container');
- const pre = parent.prev();
-
- const el = pre.find('.js-render-mermaid');
-
- parent.remove();
-
- // sandbox update
- const element = el.get(0);
- const { source } = fixElementSource(element);
+export default function renderMermaid(els) {
+ if (!els.length) return;
- renderMermaidEl(element, source);
- });
-});
-
-export default function renderMermaid($els) {
- if (!$els.length) return;
+ const visibleMermaids = [];
+ const hiddenMermaids = [];
- const visibleMermaids = $els.filter(function filter() {
- return $(this).closest('details').length === 0 && $(this).is(':visible');
- });
+ for (const el of els) {
+ if (isVisibleMermaid(el)) {
+ visibleMermaids.push(el);
+ } else {
+ hiddenMermaids.push(el);
+ }
+ }
renderMermaids(visibleMermaids);
- $els.closest('details').one('toggle', function toggle() {
- if (this.open) {
- renderMermaids($(this).find('.js-render-mermaid'));
- }
+ hiddenMermaids.forEach((el) => {
+ el.closest('details')?.addEventListener(
+ 'toggle',
+ ({ target: details }) => {
+ if (details.open) {
+ renderMermaids([...details.querySelectorAll('.js-render-mermaid')]);
+ }
+ },
+ {
+ once: true,
+ },
+ );
});
-
- hookLazyRenderMermaidEvent();
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 3239375bf7c..38ee02938cc 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -93,6 +93,12 @@ export const GO_TO_YOUR_MERGE_REQUESTS = {
defaultKeys: ['shift+m'],
};
+export const GO_TO_YOUR_REVIEW_REQUESTS = {
+ id: 'globalShortcuts.goToYourReviewRequests',
+ description: __('Go to your review requests'),
+ defaultKeys: ['shift+r'],
+};
+
export const GO_TO_YOUR_TODO_LIST = {
id: 'globalShortcuts.goToYourTodoList',
description: __('Go to your To-Do list'),
@@ -523,6 +529,7 @@ export const GLOBAL_SHORTCUTS_GROUP = {
FOCUS_FILTER_BAR,
GO_TO_YOUR_ISSUES,
GO_TO_YOUR_MERGE_REQUESTS,
+ GO_TO_YOUR_REVIEW_REQUESTS,
GO_TO_YOUR_TODO_LIST,
TOGGLE_PERFORMANCE_BAR,
HIDE_APPEARING_CONTENT,
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 4d78c7b56a0..7a1577e97d5 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -24,6 +24,7 @@ import {
GO_TO_MILESTONE_LIST,
GO_TO_YOUR_SNIPPETS,
GO_TO_PROJECT_FIND_FILE,
+ GO_TO_YOUR_REVIEW_REQUESTS,
} from './keybindings';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
@@ -94,6 +95,9 @@ export default class Shortcuts {
Mousetrap.bind(keysFor(GO_TO_YOUR_MERGE_REQUESTS), () =>
findAndFollowLink('.dashboard-shortcuts-merge_requests'),
);
+ Mousetrap.bind(keysFor(GO_TO_YOUR_REVIEW_REQUESTS), () =>
+ findAndFollowLink('.dashboard-shortcuts-review_requests'),
+ );
Mousetrap.bind(keysFor(GO_TO_YOUR_PROJECTS), () =>
findAndFollowLink('.dashboard-shortcuts-projects'),
);
diff --git a/app/assets/javascripts/blob/blob_blame_link.js b/app/assets/javascripts/blob/blob_blame_link.js
index 41dfd7b82b8..a5191a3d513 100644
--- a/app/assets/javascripts/blob/blob_blame_link.js
+++ b/app/assets/javascripts/blob/blob_blame_link.js
@@ -1,4 +1,6 @@
-function addBlameLink(containerSelector, linkClass) {
+import { getPageParamValue, getPageSearchString } from './utils';
+
+export function addBlameLink(containerSelector, linkClass) {
const containerEl = document.querySelector(containerSelector);
if (!containerEl) {
@@ -13,10 +15,14 @@ function addBlameLink(containerSelector, linkClass) {
lineLinkCopy.classList.remove(linkClass, 'diff-line-num');
const { lineNumber } = lineLink.dataset;
- const { blamePath } = document.querySelector('.line-numbers').dataset;
const blameLink = document.createElement('a');
+ const { blamePath } = document.querySelector('.line-numbers').dataset;
blameLink.classList.add('file-line-blame');
- blameLink.href = `${blamePath}#L${lineNumber}`;
+ const blamePerPage = document.querySelector('.js-per-page')?.dataset?.blamePerPage;
+ const pageNumber = getPageParamValue(lineNumber, blamePerPage);
+ const searchString = getPageSearchString(blamePath, pageNumber);
+
+ blameLink.href = `${blamePath}${searchString}#L${lineNumber}`;
const wrapper = document.createElement('div');
wrapper.classList.add('line-links', 'diff-line-num');
@@ -27,5 +33,3 @@ function addBlameLink(containerSelector, linkClass) {
}
});
}
-
-export default addBlameLink;
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
deleted file mode 100644
index 387d6043315..00000000000
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/* eslint-disable func-names */
-
-import Dropzone from 'dropzone';
-import $ from 'jquery';
-import { sprintf, __ } from '~/locale';
-import { HIDDEN_CLASS } from '../lib/utils/constants';
-import csrf from '../lib/utils/csrf';
-import { visitUrl } from '../lib/utils/url_utility';
-
-Dropzone.autoDiscover = false;
-
-function toggleLoading($el, $icon, loading) {
- if (loading) {
- $el.disable();
- $icon.removeClass(HIDDEN_CLASS);
- } else {
- $el.enable();
- $icon.addClass(HIDDEN_CLASS);
- }
-}
-export default class BlobFileDropzone {
- constructor(form, method) {
- const formDropzone = form.find('.dropzone');
- const submitButton = form.find('#submit-all');
- const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
- const dropzoneMessage = form.find('.dz-message');
- Dropzone.autoDiscover = false;
-
- const dropzone = formDropzone.dropzone({
- autoDiscover: false,
- autoProcessQueue: false,
- url: form.attr('action'),
- // Rails uses a hidden input field for PUT
- method,
- clickable: true,
- uploadMultiple: false,
- paramName: 'file',
- maxFilesize: gon.max_file_size || 10,
- parallelUploads: 1,
- maxFiles: 1,
- addRemoveLinks: true,
- previewsContainer: '.dropzone-previews',
- headers: csrf.headers,
- init() {
- this.on('processing', function () {
- this.options.url = form.attr('action');
- });
-
- this.on('addedfile', () => {
- toggleLoading(submitButton, submitButtonLoadingIcon, false);
- dropzoneMessage.addClass(HIDDEN_CLASS);
- $('.dropzone-alerts').html('').hide();
- });
- this.on('removedfile', () => {
- toggleLoading(submitButton, submitButtonLoadingIcon, false);
- dropzoneMessage.removeClass(HIDDEN_CLASS);
- });
- this.on('success', (header, response) => {
- $('#modal-upload-blob').modal('hide');
- visitUrl(response.filePath);
- });
- this.on('maxfilesexceeded', function (file) {
- dropzoneMessage.addClass(HIDDEN_CLASS);
- this.removeFile(file);
- });
- this.on('sending', (file, xhr, formData) => {
- formData.append('branch_name', form.find('.js-branch-name').val());
- formData.append('create_merge_request', form.find('.js-create-merge-request').val());
- formData.append('commit_message', form.find('.js-commit-message').val());
- });
- },
- // Override behavior of adding error underneath preview
- error(file, errorMessage) {
- const stripped = $('<div/>').html(errorMessage).text();
- $('.dropzone-alerts')
- .html(sprintf(__('Error uploading file: %{stripped}'), { stripped }))
- .show();
- this.removeFile(file);
- },
- });
-
- submitButton.on('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
-
- if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
- // eslint-disable-next-line no-alert
- alert(__('Please select a file'));
- return false;
- }
- toggleLoading(submitButton, submitButtonLoadingIcon, true);
- dropzone[0].dropzone.processQueue();
- return false;
- });
- }
-}
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index df38c5400e2..928035d7c1e 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -1,4 +1,5 @@
import { getLocationHash } from '../lib/utils/url_utility';
+import { getPageParamValue, getPageSearchString } from './utils';
const lineNumberRe = /^(L|LC)[0-9]+/;
@@ -16,7 +17,10 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
permalinkButton.dataset.originalHref = href;
return href;
})();
- permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`);
+ const lineNum = parseInt(hash.split('L')[1], 10);
+ const page = getPageParamValue(lineNum);
+ const searchString = getPageSearchString(baseHref, page);
+ permalinkButton.setAttribute('href', `${baseHref}${searchString}${hashUrlString}`);
});
}
};
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue
deleted file mode 100644
index 0e670bbd80a..00000000000
--- a/app/assets/javascripts/blob/components/blob_edit_content.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { debounce } from 'lodash';
-import { initSourceEditor } from '~/blob/utils';
-import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
-
-import eventHub from './eventhub';
-
-export default {
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- fileName: {
- type: String,
- required: false,
- default: '',
- },
- // This is used to help uniquely create a monaco model
- // even if two blob's share a file path.
- fileGlobalId: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- editor: null,
- };
- },
- watch: {
- fileName(newVal) {
- this.editor.updateModelLanguage(newVal);
- },
- },
- mounted() {
- this.editor = initSourceEditor({
- el: this.$refs.editor,
- blobPath: this.fileName,
- blobContent: this.value,
- blobGlobalId: this.fileGlobalId,
- });
-
- this.editor.onDidChangeModelContent(debounce(this.onFileChange.bind(this), 250));
-
- eventHub.$emit(SNIPPET_MEASURE_BLOBS_CONTENT);
- },
- beforeDestroy() {
- this.editor.dispose();
- },
- methods: {
- onFileChange() {
- this.$emit('input', this.editor.getValue());
- },
- },
-};
-</script>
-<template>
- <div class="file-content code">
- <div id="editor" ref="editor" data-editor-loading>
- <pre class="editor-loading-content">{{ value }}</pre>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index bbc061dd36e..b2400a4b224 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -1,16 +1,18 @@
-import Editor from '~/editor/source_editor';
+import { getBaseURL } from '~/lib/utils/url_utility';
-export function initSourceEditor({ el, ...args }) {
- const editor = new Editor({
- scrollbar: {
- alwaysConsumeMouseWheel: false,
- },
- });
+const blameLinesPerPage = document.querySelector('.js-per-page')?.dataset?.blamePerPage;
- return editor.createInstance({
- el,
- ...args,
- });
-}
+export const getPageParamValue = (lineNum, blamePerPage = blameLinesPerPage) => {
+ if (!blamePerPage) return '';
+ const page = Math.ceil(parseInt(lineNum, 10) / parseInt(blamePerPage, 10));
+ return page <= 1 ? '' : page;
+};
+
+export const getPageSearchString = (path, page) => {
+ if (!page) return '';
+ const url = new URL(path, getBaseURL());
+ url.searchParams.set('page', page);
+ return url.search;
+};
export default () => ({});
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 1c9c99dcc2f..4741dd53708 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -3,9 +3,8 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { createAlert } from '~/flash';
-import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
+import { setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
-import BlobFileDropzone from '../blob/blob_file_dropzone';
import NewCommitForm from '../new_commit_form';
const initPopovers = () => {
@@ -38,21 +37,8 @@ const initPopovers = () => {
}
};
-export const initUploadForm = () => {
- const uploadBlobForm = $('.js-upload-blob-form');
- if (uploadBlobForm.length) {
- const method = uploadBlobForm.data('method');
-
- new BlobFileDropzone(uploadBlobForm, method);
- new NewCommitForm(uploadBlobForm);
-
- disableButtonIfEmptyField(uploadBlobForm.find('.js-commit-message'), '.btn-upload-file');
- }
-};
-
export default () => {
const editBlobForm = $('.js-edit-blob-form');
- const deleteBlobForm = $('.js-delete-blob-form');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
@@ -99,10 +85,4 @@ export default () => {
// returning here blocks page navigation
window.onbeforeunload = () => '';
}
-
- initUploadForm();
-
- if (deleteBlobForm.length) {
- new NewCommitForm(deleteBlobForm);
- }
};
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 78e3f934183..97d8b206307 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -16,8 +16,10 @@ export default class EditBlob {
constructor(options) {
this.options = options;
this.configureMonacoEditor();
+ this.isMarkdown = this.options.isMarkdown;
+ this.markdownLivePreviewOpened = false;
- if (this.options.isMarkdown) {
+ if (this.isMarkdown) {
this.fetchMarkdownExtension();
}
@@ -104,6 +106,13 @@ export default class EditBlob {
this.$editModeLinks.on('click', (e) => this.editModeLinkClickHandler(e));
}
+ toggleMarkdownPreview(toOpen) {
+ if (toOpen !== this.markdownLivePreviewOpened) {
+ this.editor.markdownPreview?.eventEmitter.fire();
+ this.markdownLivePreviewOpened = !this.markdownLivePreviewOpened;
+ }
+ }
+
editModeLinkClickHandler(e) {
e.preventDefault();
@@ -115,25 +124,29 @@ export default class EditBlob {
currentLink.parent().addClass('active hover');
- this.$editModePanes.hide();
-
- currentPane.show();
-
- if (paneId === '#preview') {
- this.$toggleButton.hide();
- axios
- .post(currentLink.data('previewUrl'), {
- content: this.editor.getValue(),
- })
- .then(({ data }) => {
- currentPane.empty().append(data);
- currentPane.renderGFM();
- })
- .catch(() =>
- createAlert({
- message: BLOB_PREVIEW_ERROR,
- }),
- );
+ if (this.isMarkdown) {
+ this.toggleMarkdownPreview(paneId === '#preview');
+ } else {
+ this.$editModePanes.hide();
+
+ currentPane.show();
+
+ if (paneId === '#preview') {
+ this.$toggleButton.hide();
+ axios
+ .post(currentLink.data('previewUrl'), {
+ content: this.editor.getValue(),
+ })
+ .then(({ data }) => {
+ currentPane.empty().append(data);
+ currentPane.renderGFM();
+ })
+ .catch(() =>
+ createAlert({
+ message: BLOB_PREVIEW_ERROR,
+ }),
+ );
+ }
}
this.$toggleButton.show();
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index af753151be8..1335a3b108b 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -11,7 +11,7 @@ export default {
BoardSettingsSidebar,
BoardTopBar,
},
- inject: ['disabled'],
+ inject: ['disabled', 'fullBoardId'],
computed: {
...mapGetters(['isSidebarOpen']),
},
@@ -27,7 +27,7 @@ export default {
<template>
<div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
<board-top-bar />
- <board-content :disabled="disabled" />
+ <board-content :disabled="disabled" :board-id="fullBoardId" />
<board-settings-sidebar />
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 44c16324950..f3307977be9 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -109,6 +109,8 @@ export default {
:update-filters="true"
:index="index"
:show-work-item-type-icon="showWorkItemTypeIcon"
- />
+ >
+ <slot></slot>
+ </board-card-inner>
</li>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 3a2b11a649d..05c786ca61d 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -15,7 +15,6 @@ import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
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 BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
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';
@@ -36,7 +35,6 @@ export default {
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
IssuableBlockedIcon,
GlSprintf,
- BoardCardMoveToPosition,
WorkItemTypeIcon,
IssueHealthStatus: () =>
import('ee_component/related_items_tree/components/issue_health_status.vue'),
@@ -45,7 +43,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [boardCardInner],
- inject: ['rootPath', 'scopedLabelsAvailable'],
+ inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard'],
props: {
item: {
type: Object,
@@ -80,7 +78,7 @@ export default {
},
computed: {
...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
- ...mapGetters(['isEpicBoard', 'isProjectBoard']),
+ ...mapGetters(['isProjectBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
@@ -250,8 +248,7 @@ export default {
>{{ item.title }}</a
>
</h4>
- <!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
- <board-card-move-to-position v-if="!isEpicBoard" :item="item" :list="list" :index="index" />
+ <slot></slot>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
diff --git a/app/assets/javascripts/boards/components/board_card_move_to_position.vue b/app/assets/javascripts/boards/components/board_card_move_to_position.vue
index ff938219475..706b453e868 100644
--- a/app/assets/javascripts/boards/components/board_card_move_to_position.vue
+++ b/app/assets/javascripts/boards/components/board_card_move_to_position.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -31,10 +31,13 @@ export default {
type: Number,
required: true,
},
+ listItemsLength: {
+ type: Number,
+ required: true,
+ },
},
computed: {
...mapState(['pageInfoByListId']),
- ...mapGetters(['getBoardItemsByList']),
tracking() {
return {
category: 'boards:list',
@@ -42,15 +45,9 @@ export default {
property: `type_card`,
};
},
- listItems() {
- return this.getBoardItemsByList(this.list.id);
- },
listHasNextPage() {
return this.pageInfoByListId[this.list.id]?.hasNextPage;
},
- lengthOfListItemsInBoard() {
- return this.listItems?.length;
- },
itemIdentifier() {
return `${this.item.id}-${this.item.iid}-${this.index}`;
},
@@ -58,7 +55,7 @@ export default {
return this.index === 0;
},
isLastItemInList() {
- return this.index === this.lengthOfListItemsInBoard - 1;
+ return this.index === this.listItemsLength - 1;
},
},
methods: {
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index d99afa8455d..150378f7a7d 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,14 +1,21 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import { sortBy } from 'lodash';
+import { sortBy, throttle } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
+import { s__ } from '~/locale';
+import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { defaultSortableOptions } from '~/sortable/constants';
-import { DraggableItemTypes } from '../constants';
+import { DraggableItemTypes, BoardType, listsQuery } from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue';
export default {
+ i18n: {
+ fetchError: s__(
+ 'Boards|An error occurred while fetching the board lists. Please reload the page.',
+ ),
+ },
draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
@@ -19,21 +26,78 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
- inject: ['canAdminList'],
+ inject: [
+ 'canAdminList',
+ 'boardType',
+ 'fullPath',
+ 'issuableType',
+ 'isIssueBoard',
+ 'isEpicBoard',
+ 'isApolloBoard',
+ ],
props: {
disabled: {
type: Boolean,
required: true,
},
+ boardId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ boardHeight: null,
+ boardListsApollo: {},
+ apolloError: null,
+ updatedBoardId: this.boardId,
+ };
+ },
+ apollo: {
+ boardListsApollo: {
+ query() {
+ return listsQuery[this.issuableType].query;
+ },
+ variables() {
+ return this.queryVariables;
+ },
+ skip() {
+ return !this.isApolloBoard;
+ },
+ update(data) {
+ const { lists } = data[this.boardType].board;
+ return formatBoardLists(lists);
+ },
+ result() {
+ // this allows us to delay fetching lists when we switch a board to fetch the actual board lists
+ // instead of fetching lists for the "previous" board
+ this.updatedBoardId = this.boardId;
+ },
+ error() {
+ this.apolloError = this.$options.i18n.fetchError;
+ },
+ },
},
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
- ...mapGetters(['isSwimlanesOn', 'isEpicBoard', 'isIssueBoard']),
+ ...mapGetters(['isSwimlanesOn']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
+ queryVariables() {
+ return {
+ ...(this.isIssueBoard && {
+ isGroup: this.boardType === BoardType.group,
+ isProject: this.boardType === BoardType.project,
+ }),
+ fullPath: this.fullPath,
+ boardId: this.boardId,
+ filterParams: this.filterParams,
+ };
+ },
boardListsToUse() {
- return sortBy([...Object.values(this.boardLists)], 'position');
+ const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists;
+ return sortBy([...Object.values(lists)], 'position');
},
canDragColumns() {
return this.canAdminList;
@@ -54,6 +118,22 @@ export default {
return this.canDragColumns ? options : {};
},
+ errorToDisplay() {
+ return this.isApolloBoard ? this.apolloError : this.error;
+ },
+ },
+ mounted() {
+ this.setBoardHeight();
+
+ this.resizeObserver = new ResizeObserver(
+ throttle(() => {
+ this.setBoardHeight();
+ }, 150),
+ );
+ this.resizeObserver.observe(document.body);
+ },
+ unmounted() {
+ this.resizeObserver.disconnect();
},
methods: {
...mapActions(['moveList', 'unsetError']),
@@ -61,14 +141,17 @@ export default {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
+ setBoardHeight() {
+ this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`;
+ },
},
};
</script>
<template>
<div v-cloak data-qa-selector="boards_list">
- <gl-alert v-if="error" variant="danger" :dismissible="true" @dismiss="unsetError">
- {{ error }}
+ <gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="unsetError">
+ {{ errorToDisplay }}
</gl-alert>
<component
:is="boardColumnWrapper"
@@ -76,6 +159,7 @@ export default {
ref="list"
v-bind="draggableOptions"
class="boards-list gl-w-full gl-py-5 gl-pr-3 gl-white-space-nowrap gl-overflow-x-scroll"
+ :style="{ height: boardHeight }"
@end="moveList"
>
<board-column
@@ -99,6 +183,7 @@ export default {
:lists="boardListsToUse"
:can-admin-list="canAdminList"
:disabled="disabled"
+ :style="{ height: boardHeight }"
/>
<board-content-sidebar v-if="isIssueBoard" data-testid="issue-boards-sidebar" />
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 11a5d89cc8c..97f52f21e7f 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -6,9 +6,20 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
- FILTERED_SEARCH_TERM,
FILTER_ANY,
+ FILTERED_SEARCH_TERM,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_EPIC,
TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { AssigneeFilterType } from '~/boards/constants';
@@ -17,8 +28,6 @@ import eventHub from '../eventhub';
export default {
i18n: {
search: __('Search'),
- label: __('Label'),
- author: __('Author'),
},
components: { FilteredSearch },
inject: ['initialFilterParams'],
@@ -62,28 +71,28 @@ export default {
if (authorUsername) {
filteredSearchValue.push({
- type: 'author',
+ type: TOKEN_TYPE_AUTHOR,
value: { data: authorUsername, operator: '=' },
});
}
if (assigneeUsername) {
filteredSearchValue.push({
- type: 'assignee',
+ type: TOKEN_TYPE_ASSIGNEE,
value: { data: assigneeUsername, operator: '=' },
});
}
if (assigneeId) {
filteredSearchValue.push({
- type: 'assignee',
+ type: TOKEN_TYPE_ASSIGNEE,
value: { data: assigneeId, operator: '=' },
});
}
if (types) {
filteredSearchValue.push({
- type: 'type',
+ type: TOKEN_TYPE_TYPE,
value: { data: types, operator: '=' },
});
}
@@ -91,7 +100,7 @@ export default {
if (labelName?.length) {
filteredSearchValue.push(
...labelName.map((label) => ({
- type: 'label',
+ type: TOKEN_TYPE_LABEL,
value: { data: label, operator: '=' },
})),
);
@@ -99,7 +108,7 @@ export default {
if (milestoneTitle) {
filteredSearchValue.push({
- type: 'milestone',
+ type: TOKEN_TYPE_MILESTONE,
value: { data: milestoneTitle, operator: '=' },
});
}
@@ -116,42 +125,42 @@ export default {
if (iterationData) {
filteredSearchValue.push({
- type: 'iteration',
+ type: TOKEN_TYPE_ITERATION,
value: { data: iterationData, operator: '=' },
});
}
if (weight) {
filteredSearchValue.push({
- type: 'weight',
+ type: TOKEN_TYPE_WEIGHT,
value: { data: weight, operator: '=' },
});
}
if (myReactionEmoji) {
filteredSearchValue.push({
- type: 'my-reaction',
+ type: TOKEN_TYPE_MY_REACTION,
value: { data: myReactionEmoji, operator: '=' },
});
}
if (releaseTag) {
filteredSearchValue.push({
- type: 'release',
+ type: TOKEN_TYPE_RELEASE,
value: { data: releaseTag, operator: '=' },
});
}
if (confidential !== undefined) {
filteredSearchValue.push({
- type: 'confidential',
+ type: TOKEN_TYPE_CONFIDENTIAL,
value: { data: confidential },
});
}
if (epicId) {
filteredSearchValue.push({
- type: 'epic',
+ type: TOKEN_TYPE_EPIC,
value: { data: epicId, operator: '=' },
});
}
@@ -165,35 +174,35 @@ export default {
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
- type: 'author',
+ type: TOKEN_TYPE_AUTHOR,
value: { data: this.filterParams['not[authorUsername]'], operator: '!=' },
});
}
if (this.filterParams['not[milestoneTitle]']) {
filteredSearchValue.push({
- type: 'milestone',
+ type: TOKEN_TYPE_MILESTONE,
value: { data: this.filterParams['not[milestoneTitle]'], operator: '!=' },
});
}
if (this.filterParams['not[iterationId]']) {
filteredSearchValue.push({
- type: 'iteration',
+ type: TOKEN_TYPE_ITERATION,
value: { data: this.filterParams['not[iterationId]'], operator: '!=' },
});
}
if (this.filterParams['not[weight]']) {
filteredSearchValue.push({
- type: 'weight',
+ type: TOKEN_TYPE_WEIGHT,
value: { data: this.filterParams['not[weight]'], operator: '!=' },
});
}
if (this.filterParams['not[assigneeUsername]']) {
filteredSearchValue.push({
- type: 'assignee',
+ type: TOKEN_TYPE_ASSIGNEE,
value: { data: this.filterParams['not[assigneeUsername]'], operator: '!=' },
});
}
@@ -201,7 +210,7 @@ export default {
if (this.filterParams['not[labelName]']) {
filteredSearchValue.push(
...this.filterParams['not[labelName]'].map((label) => ({
- type: 'label',
+ type: TOKEN_TYPE_LABEL,
value: { data: label, operator: '!=' },
})),
);
@@ -209,28 +218,28 @@ export default {
if (this.filterParams['not[types]']) {
filteredSearchValue.push({
- type: 'type',
+ type: TOKEN_TYPE_TYPE,
value: { data: this.filterParams['not[types]'], operator: '!=' },
});
}
if (this.filterParams['not[epicId]']) {
filteredSearchValue.push({
- type: 'epic',
+ type: TOKEN_TYPE_EPIC,
value: { data: this.filterParams['not[epicId]'], operator: '!=' },
});
}
if (this.filterParams['not[myReactionEmoji]']) {
filteredSearchValue.push({
- type: 'my-reaction',
+ type: TOKEN_TYPE_MY_REACTION,
value: { data: this.filterParams['not[myReactionEmoji]'], operator: '!=' },
});
}
if (this.filterParams['not[releaseTag]']) {
filteredSearchValue.push({
- type: 'release',
+ type: TOKEN_TYPE_RELEASE,
value: { data: this.filterParams['not[releaseTag]'], operator: '!=' },
});
}
@@ -302,7 +311,7 @@ export default {
my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag,
confidential,
- [TOKEN_TYPE_HEALTH]: healthStatus,
+ health_status: healthStatus,
},
(value) => {
if (value || value === false) {
@@ -361,44 +370,44 @@ export default {
filters.forEach((filter) => {
switch (filter.type) {
- case 'author':
+ case TOKEN_TYPE_AUTHOR:
filterParams.authorUsername = filter.value.data;
break;
- case 'assignee':
+ case TOKEN_TYPE_ASSIGNEE:
if (Object.values(AssigneeFilterType).includes(filter.value.data)) {
filterParams.assigneeId = filter.value.data;
} else {
filterParams.assigneeUsername = filter.value.data;
}
break;
- case 'type':
+ case TOKEN_TYPE_TYPE:
filterParams.types = filter.value.data;
break;
- case 'label':
+ case TOKEN_TYPE_LABEL:
labels.push(filter.value.data);
break;
- case 'milestone':
+ case TOKEN_TYPE_MILESTONE:
filterParams.milestoneTitle = filter.value.data;
break;
- case 'iteration':
+ case TOKEN_TYPE_ITERATION:
filterParams.iterationId = filter.value.data;
break;
- case 'weight':
+ case TOKEN_TYPE_WEIGHT:
filterParams.weight = filter.value.data;
break;
- case 'epic':
+ case TOKEN_TYPE_EPIC:
filterParams.epicId = filter.value.data;
break;
- case 'my-reaction':
+ case TOKEN_TYPE_MY_REACTION:
filterParams.myReactionEmoji = filter.value.data;
break;
- case 'release':
+ case TOKEN_TYPE_RELEASE:
filterParams.releaseTag = filter.value.data;
break;
- case 'confidential':
+ case TOKEN_TYPE_CONFIDENTIAL:
filterParams.confidential = filter.value.data;
break;
- case 'filtered-search-term':
+ case FILTERED_SEARCH_TERM:
if (filter.value.data) plainText.push(filter.value.data);
break;
case TOKEN_TYPE_HEALTH:
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index eb889344c1e..fcf026bbe00 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -84,7 +84,7 @@ export default {
},
computed: {
...mapState(['error']),
- ...mapGetters(['isIssueBoard', 'isGroupBoard', 'isProjectBoard']),
+ ...mapGetters(['isGroupBoard', 'isProjectBoard']),
isNewForm() {
return this.currentPage === formType.new;
},
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index edf1a5ee7e6..816b22e4dc6 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,12 +1,13 @@
<script>
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { sprintf, __ } from '~/locale';
import { defaultSortableOptions } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
import Tracking from '~/tracking';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
+import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import { toggleFormEventPrefix, DraggableItemTypes } from '../constants';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
@@ -27,8 +28,10 @@ export default {
BoardNewEpic: () => import('ee_component/boards/components/board_new_epic.vue'),
GlLoadingIcon,
GlIntersectionObserver,
+ BoardCardMoveToPosition,
},
mixins: [Tracking.mixin()],
+ inject: ['isEpicBoard'],
props: {
disabled: {
type: Boolean,
@@ -60,6 +63,9 @@ export default {
filters: this.filterParams,
};
},
+ context: {
+ isSingleRequest: true,
+ },
skip() {
return this.isEpicBoard;
},
@@ -67,7 +73,6 @@ export default {
},
computed: {
...mapState(['pageInfoByListId', 'listsFlags', 'filterParams', 'isUpdateIssueOrderInProgress']),
- ...mapGetters(['isEpicBoard']),
listItemsCount() {
return this.isEpicBoard ? this.list.epicsCount : this.boardList?.issuesCount;
},
@@ -309,7 +314,16 @@ export default {
:data-draggable-item-type="$options.draggableItemTypes.card"
:disabled="disabled"
:show-work-item-type-icon="!isEpicBoard"
- />
+ >
+ <!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
+ <board-card-move-to-position
+ v-if="!isEpicBoard"
+ :item="item"
+ :index="index"
+ :list="list"
+ :list-items-length="boardItems.length"
+ />
+ </board-card>
<gl-intersection-observer @appear="onReachingListBottom">
<li
v-if="showCount"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 230fa4e1e0f..bfc4b52baaf 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -57,6 +57,9 @@ export default {
canCreateEpic: {
default: false,
},
+ isEpicBoard: {
+ default: false,
+ },
},
props: {
list: {
@@ -76,7 +79,7 @@ export default {
},
computed: {
...mapState(['activeId', 'filterParams', 'boardId']),
- ...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
+ ...mapGetters(['isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
@@ -168,6 +171,9 @@ export default {
filters: this.filterParams,
};
},
+ context: {
+ isSingleRequest: true,
+ },
skip() {
return this.isEpicBoard;
},
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index e93edad675c..c0c2699b63d 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -31,7 +31,7 @@ export default {
GlModal: GlModalDirective,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['canAdminList', 'scopedLabelsAvailable'],
+ inject: ['canAdminList', 'scopedLabelsAvailable', 'isIssueBoard'],
inheritAttrs: false,
data() {
return {
@@ -40,10 +40,10 @@ export default {
},
modalId: 'board-settings-sidebar-modal',
computed: {
- ...mapGetters(['isSidebarOpen', 'isEpicBoard']),
+ ...mapGetters(['isSidebarOpen']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
- return this.glFeatures.wipLimits && !this.isEpicBoard;
+ return this.glFeatures.wipLimits && this.isIssueBoard;
},
activeList() {
return this.boardLists[this.activeId] || {};
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 54a6e3000a4..368feba9a44 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -1,5 +1,4 @@
<script>
-import { mapGetters } from 'vuex';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
import IssueBoardFilteredSearch from 'ee_else_ce/boards/components/issue_board_filtered_search.vue';
@@ -20,10 +19,7 @@ export default {
EpicBoardFilteredSearch: () =>
import('ee_component/boards/components/epic_filtered_search.vue'),
},
- inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn'],
- computed: {
- ...mapGetters(['isEpicBoard']),
- },
+ inject: ['swimlanesFeatureAvailable', 'canAdminList', 'isSignedIn', 'isIssueBoard'],
};
</script>
@@ -37,8 +33,8 @@ export default {
>
<boards-selector />
<new-board-button />
- <epic-board-filtered-search v-if="isEpicBoard" />
- <issue-board-filtered-search v-else />
+ <issue-board-filtered-search v-if="isIssueBoard" />
+ <epic-board-filtered-search v-else />
</div>
<div
class="filter-dropdown-container gl-md-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-align-items-flex-start"
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 bab6fe26978..605e11d1590 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -12,9 +12,24 @@ import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import {
- TOKEN_TITLE_MY_REACTION,
OPERATOR_IS_AND_IS_NOT,
OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_CONFIDENTIAL,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_TYPE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
@@ -28,17 +43,8 @@ export default {
INCIDENT: 'INCIDENT',
},
i18n: {
- search: __('Search'),
- epic: __('Epic'),
- label: __('Label'),
- author: __('Author'),
- assignee: __('Assignee'),
- type: __('Type'),
incident: __('Incident'),
issue: __('Issue'),
- milestone: __('Milestone'),
- release: __('Release'),
- confidential: __('Confidential'),
},
components: { BoardFilteredSearch },
inject: ['isSignedIn', 'releasesFetchPath', 'fullPath', 'boardType'],
@@ -52,17 +58,7 @@ export default {
: this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
},
tokensCE() {
- const {
- label,
- author,
- assignee,
- issue,
- incident,
- type,
- milestone,
- release,
- confidential,
- } = this.$options.i18n;
+ const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
const { fetchAuthors, fetchLabels } = issueBoardFilters(
this.$apollo,
@@ -73,8 +69,8 @@ export default {
const tokens = [
{
icon: 'user',
- title: assignee,
- type: 'assignee',
+ title: TOKEN_TITLE_ASSIGNEE,
+ type: TOKEN_TYPE_ASSIGNEE,
operators: OPERATOR_IS_AND_IS_NOT,
token: AuthorToken,
unique: true,
@@ -83,8 +79,8 @@ export default {
},
{
icon: 'pencil',
- title: author,
- type: 'author',
+ title: TOKEN_TITLE_AUTHOR,
+ type: TOKEN_TYPE_AUTHOR,
operators: OPERATOR_IS_AND_IS_NOT,
symbol: '@',
token: AuthorToken,
@@ -94,8 +90,8 @@ export default {
},
{
icon: 'labels',
- title: label,
- type: 'label',
+ title: TOKEN_TITLE_LABEL,
+ type: TOKEN_TYPE_LABEL,
operators: OPERATOR_IS_AND_IS_NOT,
token: LabelToken,
unique: false,
@@ -105,7 +101,7 @@ export default {
...(this.isSignedIn
? [
{
- type: 'my-reaction',
+ type: TOKEN_TYPE_MY_REACTION,
title: TOKEN_TITLE_MY_REACTION,
icon: 'thumb-up',
token: EmojiToken,
@@ -127,9 +123,9 @@ export default {
},
},
{
- type: 'confidential',
+ type: TOKEN_TYPE_CONFIDENTIAL,
icon: 'eye-slash',
- title: confidential,
+ title: TOKEN_TITLE_CONFIDENTIAL,
unique: true,
token: GlFilteredSearchToken,
operators: OPERATOR_IS_ONLY,
@@ -141,8 +137,8 @@ export default {
]
: []),
{
- type: 'milestone',
- title: milestone,
+ type: TOKEN_TYPE_MILESTONE,
+ title: TOKEN_TITLE_MILESTONE,
icon: 'clock',
symbol: '%',
token: MilestoneToken,
@@ -152,8 +148,8 @@ export default {
},
{
icon: 'issues',
- title: type,
- type: 'type',
+ title: TOKEN_TITLE_TYPE,
+ type: TOKEN_TYPE_TYPE,
token: GlFilteredSearchToken,
unique: true,
options: [
@@ -162,8 +158,8 @@ export default {
],
},
{
- type: 'release',
- title: release,
+ type: TOKEN_TYPE_RELEASE,
+ title: TOKEN_TITLE_RELEASE,
icon: 'rocket',
token: ReleaseToken,
fetchReleases: (search) => {
diff --git a/app/assets/javascripts/boards/graphql.js b/app/assets/javascripts/boards/graphql.js
deleted file mode 100644
index d066a5d002e..00000000000
--- a/app/assets/javascripts/boards/graphql.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { defaultDataIdFromObject } from '@apollo/client/core';
-import createDefaultClient from '~/lib/graphql';
-
-export const gqlClient = createDefaultClient(
- {},
- {
- cacheConfig: {
- dataIdFromObject: (object) => {
- // eslint-disable-next-line no-underscore-dangle
- return object.__typename === 'BoardList' ? object.iid : defaultDataIdFromObject(object);
- },
- },
- batchMax: 2,
- },
-);
diff --git a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
index f48383624c9..d5498f03e4b 100644
--- a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
@@ -1,4 +1,4 @@
-query BoardList($id: ListID!, $filters: BoardIssueInput) {
+query BoardListCount($id: ListID!, $filters: BoardIssueInput) {
boardList(id: $id, issueFilters: $filters) {
id
issuesCount
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index a7003edba47..f8bd81e6b98 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -12,14 +12,14 @@ import {
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
+import { defaultClient } from '~/graphql_shared/issuable_client';
import { fullBoardId } from './boards_util';
-import { gqlClient } from './graphql';
Vue.use(VueApollo);
Vue.use(PortalVue);
const apolloProvider = new VueApollo({
- defaultClient: gqlClient,
+ defaultClient,
});
function mountBoardApp(el) {
@@ -53,6 +53,8 @@ function mountBoardApp(el) {
store,
apolloProvider,
provide: {
+ isApolloBoard: window.gon?.features?.apolloBoards,
+ fullBoardId: fullBoardId(boardId),
disabled: parseBoolean(el.dataset.disabled),
groupId: Number(groupId),
rootPath,
@@ -70,6 +72,8 @@ function mountBoardApp(el) {
emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
hasMissingBoards: parseBoolean(el.dataset.hasMissingBoards),
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
+ isIssueBoard: true,
+ isEpicBoard: false,
// Permissions
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index c2e346da606..e5437690fd4 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -34,11 +34,11 @@ import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mut
import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import eventHub from '../eventhub';
-import { gqlClient } from '../graphql';
import projectBoardQuery from '../graphql/project_board.query.graphql';
import groupBoardQuery from '../graphql/group_board.query.graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
@@ -149,6 +149,9 @@ export default {
query: listsQuery[issuableType].query,
variables,
...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
+ context: {
+ isSingleRequest: true,
+ },
})
.then(({ data }) => {
const { lists, hideBacklogList } = data[boardType].board;
diff --git a/app/assets/javascripts/branches/components/delete_merged_branches.vue b/app/assets/javascripts/branches/components/delete_merged_branches.vue
new file mode 100644
index 00000000000..70974f2e725
--- /dev/null
+++ b/app/assets/javascripts/branches/components/delete_merged_branches.vue
@@ -0,0 +1,171 @@
+<script>
+import { GlButton, GlFormInput, GlModal, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { sprintf, s__, __ } from '~/locale';
+
+export const i18n = {
+ deleteButtonText: s__('Branches|Delete merged branches'),
+ buttonTooltipText: s__("Branches|Delete all branches that are merged into '%{defaultBranch}'"),
+ modalTitle: s__('Branches|Delete all merged branches?'),
+ modalMessage: s__(
+ 'Branches|You are about to %{strongStart}delete all branches%{strongEnd} that were merged into %{codeStart}%{defaultBranch}%{codeEnd}.',
+ ),
+ notVisibleBranchesWarning: s__(
+ 'Branches|This may include merged branches that are not visible on the current screen.',
+ ),
+ protectedBranchWarning: s__(
+ "Branches|A branch won't be deleted if it is protected or associated with an open merge request.",
+ ),
+ permanentEffectWarning: s__(
+ 'Branches|This bulk action is %{strongStart}permanent and cannot be undone or recovered%{strongEnd}.',
+ ),
+ confirmationMessage: s__(
+ 'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.',
+ ),
+ cancelButtonText: __('Cancel'),
+};
+
+export default {
+ csrf,
+ components: {
+ GlModal,
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ formPath: {
+ type: String,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ areAllBranchesVisible: false,
+ enteredText: '',
+ };
+ },
+ computed: {
+ buttonTooltipText() {
+ return sprintf(this.$options.i18n.buttonTooltipText, { defaultBranch: this.defaultBranch });
+ },
+ modalMessage() {
+ return sprintf(this.$options.i18n.modalMessage, {
+ defaultBranch: this.defaultBranch,
+ });
+ },
+ isDeletingConfirmed() {
+ return this.enteredText.trim().toLowerCase() === 'delete';
+ },
+ isDeleteButtonDisabled() {
+ return !this.isDeletingConfirmed;
+ },
+ },
+ methods: {
+ openModal() {
+ this.$refs.modal.show();
+ },
+ submitForm() {
+ if (!this.isDeleteButtonDisabled) {
+ this.$refs.form.submit();
+ }
+ },
+ closeModal() {
+ this.$refs.modal.hide();
+ },
+ },
+ i18n,
+};
+</script>
+
+<template>
+ <div>
+ <gl-button
+ v-gl-tooltip="buttonTooltipText"
+ class="gl-mr-3"
+ data-qa-selector="delete_merged_branches_button"
+ category="secondary"
+ variant="danger"
+ @click="openModal"
+ >{{ $options.i18n.deleteButtonText }}
+ </gl-button>
+ <gl-modal
+ ref="modal"
+ size="sm"
+ modal-id="delete-merged-branches"
+ :title="$options.i18n.modalTitle"
+ >
+ <form ref="form" :action="formPath" method="post" @submit.prevent>
+ <p>
+ <gl-sprintf :message="modalMessage">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ {{ $options.i18n.notVisibleBranchesWarning }}
+ </p>
+ <p>
+ {{ $options.i18n.protectedBranchWarning }}
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.permanentEffectWarning">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.confirmationMessage">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ <gl-form-input
+ v-model="enteredText"
+ data-qa-selector="delete_merged_branches_input"
+ type="text"
+ size="sm"
+ class="gl-mt-2"
+ aria-labelledby="input-label"
+ autocomplete="off"
+ @keyup.enter="submitForm"
+ />
+ </p>
+
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+
+ <template #modal-footer>
+ <div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3"
+ >
+ <gl-button data-testid="delete-merged-branches-cancel-button" @click="closeModal">
+ {{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ <gl-button
+ ref="deleteMergedBrancesButton"
+ :disabled="isDeleteButtonDisabled"
+ variant="danger"
+ data-qa-selector="delete_merged_branches_confirmation_button"
+ data-testid="delete-merged-branches-confirmation-button"
+ @click="submitForm"
+ >{{ $options.i18n.deleteButtonText }}</gl-button
+ >
+ </div>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/branches/init_delete_merged_branches.js b/app/assets/javascripts/branches/init_delete_merged_branches.js
new file mode 100644
index 00000000000..998db07d8de
--- /dev/null
+++ b/app/assets/javascripts/branches/init_delete_merged_branches.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import DeleteMergedBranches from '~/branches/components/delete_merged_branches.vue';
+
+export default function initDeleteMergedBranchesModal() {
+ const el = document.querySelector('.js-delete-merged-branches');
+ if (!el) {
+ return false;
+ }
+
+ const { formPath, defaultBranch } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(DeleteMergedBranches, {
+ props: {
+ formPath,
+ defaultBranch,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue b/app/assets/javascripts/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue
new file mode 100644
index 00000000000..16bfc7f3abe
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/delete_pipeline_schedule_modal.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+
+export default {
+ modal: {
+ id: 'delete-pipeline-schedule-modal',
+ deleteConfirmation: s__(
+ 'PipelineSchedules|Are you sure you want to delete this pipeline schedule?',
+ ),
+ actionPrimary: {
+ text: s__('PipelineSchedules|Delete pipeline schedule'),
+ attributes: [{ variant: 'danger' }],
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: [],
+ },
+ },
+ components: {
+ GlModal,
+ },
+ props: {
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :visible="visible"
+ :title="$options.modal.actionPrimary.text"
+ :modal-id="$options.modal.id"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ size="sm"
+ @primary="$emit('deleteSchedule')"
+ @hide="$emit('hideModal')"
+ >
+ {{ $options.modal.deleteConfirmation }}
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
new file mode 100644
index 00000000000..fe16cb7a92e
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -0,0 +1,256 @@
+<script>
+import { GlAlert, GlBadge, GlButton, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import { limitedCounterWithDelimiter } from '~/lib/utils/text_utility';
+import { queryToObject } from '~/lib/utils/url_utility';
+import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
+import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
+import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
+import TakeOwnershipModal from './take_ownership_modal.vue';
+import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
+
+export default {
+ i18n: {
+ schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
+ scheduleDeleteError: s__(
+ 'PipelineSchedules|There was a problem deleting the pipeline schedule.',
+ ),
+ takeOwnershipError: s__(
+ 'PipelineSchedules|There was a problem taking ownership of the pipeline schedule.',
+ ),
+ newSchedule: s__('PipelineSchedules|New schedule'),
+ deleteSuccess: s__('PipelineSchedules|Pipeline schedule successfully deleted.'),
+ },
+ components: {
+ DeletePipelineScheduleModal,
+ GlAlert,
+ GlBadge,
+ GlButton,
+ GlLoadingIcon,
+ GlTabs,
+ GlTab,
+ PipelineSchedulesTable,
+ TakeOwnershipModal,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ schedules: {
+ query: getPipelineSchedulesQuery,
+ variables() {
+ return {
+ projectPath: this.fullPath,
+ status: this.scope,
+ };
+ },
+ update(data) {
+ const { pipelineSchedules: { nodes: list = [], count } = {} } = data.project || {};
+
+ return {
+ list,
+ count,
+ };
+ },
+ error() {
+ this.reportError(this.$options.i18n.schedulesFetchError);
+ },
+ },
+ },
+ data() {
+ const { scope } = queryToObject(window.location.search);
+ return {
+ schedules: {
+ list: [],
+ },
+ scope,
+ hasError: false,
+ errorMessage: '',
+ scheduleId: null,
+ showDeleteModal: false,
+ showTakeOwnershipModal: false,
+ count: 0,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.schedules.loading;
+ },
+ schedulesCount() {
+ return this.schedules.count;
+ },
+ tabs() {
+ return [
+ {
+ text: s__('PipelineSchedules|All'),
+ count: limitedCounterWithDelimiter(this.count),
+ scope: null,
+ showBadge: true,
+ attrs: { 'data-testid': 'pipeline-schedules-all-tab' },
+ },
+ {
+ text: s__('PipelineSchedules|Active'),
+ scope: 'ACTIVE',
+ showBadge: false,
+ attrs: { 'data-testid': 'pipeline-schedules-active-tab' },
+ },
+ {
+ text: s__('PipelineSchedules|Inactive'),
+ scope: 'INACTIVE',
+ showBadge: false,
+ attrs: { 'data-testid': 'pipeline-schedules-inactive-tab' },
+ },
+ ];
+ },
+ },
+ watch: {
+ // this watcher ensures that the count on the all tab
+ // is not updated when switching to other tabs
+ schedulesCount(newCount) {
+ if (!this.scope) {
+ this.count = newCount;
+ }
+ },
+ },
+ methods: {
+ reportError(error) {
+ this.hasError = true;
+ this.errorMessage = error;
+ },
+ setDeleteModal(id) {
+ this.showDeleteModal = true;
+ this.scheduleId = id;
+ },
+ setTakeOwnershipModal(id) {
+ this.showTakeOwnershipModal = true;
+ this.scheduleId = id;
+ },
+ hideModal() {
+ this.showDeleteModal = false;
+ this.showTakeOwnershipModal = false;
+ this.scheduleId = null;
+ },
+ async deleteSchedule() {
+ try {
+ const {
+ data: {
+ pipelineScheduleDelete: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: deletePipelineScheduleMutation,
+ variables: { id: this.scheduleId },
+ });
+
+ if (errors.length > 0) {
+ throw new Error();
+ } else {
+ this.$apollo.queries.schedules.refetch();
+ this.$toast.show(this.$options.i18n.deleteSuccess);
+ }
+ } catch {
+ this.reportError(this.$options.i18n.scheduleDeleteError);
+ }
+ },
+ async takeOwnership() {
+ try {
+ const {
+ data: {
+ pipelineScheduleTakeOwnership: { pipelineSchedule, errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: takeOwnershipMutation,
+ variables: { id: this.scheduleId },
+ });
+
+ if (errors.length > 0) {
+ throw new Error();
+ } else {
+ this.$apollo.queries.schedules.refetch();
+
+ if (pipelineSchedule?.owner?.name) {
+ const toastMsg = sprintf(
+ s__('PipelineSchedules|Successfully taken ownership from %{owner}.'),
+ {
+ owner: pipelineSchedule.owner.name,
+ },
+ );
+
+ this.$toast.show(toastMsg);
+ }
+ }
+ } catch {
+ this.reportError(this.$options.i18n.takeOwnershipError);
+ }
+ },
+ fetchPipelineSchedulesByStatus(scope) {
+ this.scope = scope;
+ this.$apollo.queries.schedules.refetch();
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="hasError" class="gl-mb-2" variant="danger" @dismiss="hasError = false">
+ {{ errorMessage }}
+ </gl-alert>
+
+ <template v-else>
+ <gl-tabs
+ sync-active-tab-with-query-params
+ query-param-name="scope"
+ nav-class="gl-flex-grow-1 gl-align-items-center"
+ >
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.text"
+ :title-link-attributes="tab.attrs"
+ :query-param-value="tab.scope"
+ @click="fetchPipelineSchedulesByStatus(tab.scope)"
+ >
+ <template #title>
+ <span>{{ tab.text }}</span>
+
+ <template v-if="tab.showBadge">
+ <gl-loading-icon v-if="tab.scope === scope && isLoading" class="gl-ml-2" />
+
+ <gl-badge v-else-if="tab.count" size="sm" class="gl-tab-counter-badge">
+ {{ tab.count }}
+ </gl-badge>
+ </template>
+ </template>
+
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <pipeline-schedules-table
+ v-else
+ :schedules="schedules.list"
+ @showTakeOwnershipModal="setTakeOwnershipModal"
+ @showDeleteModal="setDeleteModal"
+ />
+ </gl-tab>
+
+ <template #tabs-end>
+ <gl-button variant="confirm" class="gl-ml-auto" data-testid="new-schedule-button">
+ {{ $options.i18n.newSchedule }}
+ </gl-button>
+ </template>
+ </gl-tabs>
+
+ <take-ownership-modal
+ :visible="showTakeOwnershipModal"
+ @takeOwnership="takeOwnership"
+ @hideModal="hideModal"
+ />
+
+ <delete-pipeline-schedule-modal
+ :visible="showDeleteModal"
+ @deleteSchedule="deleteSchedule"
+ @hideModal="hideModal"
+ />
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 6e24ac6b8d4..6e24ac6b8d4 100644
--- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
index 76d118bf52d..8656e5d3536 100644
--- a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -50,6 +50,8 @@ export default {
v-gl-tooltip
:title="$options.i18n.takeOwnershipTooltip"
icon="user"
+ data-testid="take-ownership-pipeline-schedule-btn"
+ @click="$emit('showTakeOwnershipModal', schedule.id)"
/>
<gl-button v-if="canUpdate" v-gl-tooltip :title="$options.i18n.editTooltip" icon="pencil" />
<gl-button
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 216796b357c..216796b357c 100644
--- a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
index 48d59bf6e7c..48d59bf6e7c 100644
--- a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
index e7fa94eb7fc..e7fa94eb7fc 100644
--- a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
diff --git a/app/assets/javascripts/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..08efa794bcc 100644
--- a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
index d54008b81b2..1b97a35a51e 100644
--- a/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
@@ -12,31 +12,37 @@ export default {
{
key: 'description',
label: s__('PipelineSchedules|Description'),
+ thClass: 'gl-border-t-none!',
columnClass: 'gl-w-40p',
},
{
key: 'target',
label: s__('PipelineSchedules|Target'),
+ thClass: 'gl-border-t-none!',
columnClass: 'gl-w-10p',
},
{
key: 'pipeline',
label: s__('PipelineSchedules|Last Pipeline'),
+ thClass: 'gl-border-t-none!',
columnClass: 'gl-w-10p',
},
{
key: 'next',
label: s__('PipelineSchedules|Next Run'),
+ thClass: 'gl-border-t-none!',
columnClass: 'gl-w-15p',
},
{
key: 'owner',
label: s__('PipelineSchedules|Owner'),
+ thClass: 'gl-border-t-none!',
columnClass: 'gl-w-10p',
},
{
key: 'actions',
label: '',
+ thClass: 'gl-border-t-none!',
columnClass: 'gl-w-15p',
},
],
@@ -88,6 +94,7 @@ export default {
<template #cell(actions)="{ item }">
<pipeline-schedule-actions
:schedule="item"
+ @showTakeOwnershipModal="$emit('showTakeOwnershipModal', $event)"
@showDeleteModal="$emit('showDeleteModal', $event)"
/>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal.vue b/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal.vue
new file mode 100644
index 00000000000..3ac52d4735d
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal.vue
@@ -0,0 +1,54 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+
+export default {
+ 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'),
+ },
+ components: {
+ GlModal,
+ },
+ props: {
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ actionCancel() {
+ return { text: this.$options.i18n.cancelLabel };
+ },
+ actionPrimary() {
+ return {
+ text: this.$options.i18n.takeOwnership,
+ attributes: [
+ {
+ variant: 'confirm',
+ category: 'primary',
+ },
+ ],
+ };
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ :visible="visible"
+ :modal-id="$options.modalId"
+ :action-primary="actionPrimary"
+ :action-cancel="actionCancel"
+ :title="$options.i18n.takeOwnership"
+ size="sm"
+ @primary="$emit('takeOwnership')"
+ @hide="$emit('hideModal')"
+ >
+ <p>{{ $options.i18n.ownershipMessage }}</p>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/take_ownership_modal.vue b/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue
index 7ded3945a32..7ded3945a32 100644
--- a/app/assets/javascripts/pipeline_schedules/components/take_ownership_modal.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/take_ownership_modal_legacy.vue
diff --git a/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
index 8aab0b3fbde..8aab0b3fbde 100644
--- a/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql
new file mode 100644
index 00000000000..e410ef91d8b
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql
@@ -0,0 +1,12 @@
+mutation takeOwnership($id: CiPipelineScheduleID!) {
+ pipelineScheduleTakeOwnership(input: { id: $id }) {
+ pipelineSchedule {
+ id
+ owner {
+ id
+ name
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
index 7d9d658b1b6..9f6cb429cca 100644
--- a/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -1,7 +1,8 @@
-query getPipelineSchedulesQuery($projectPath: ID!) {
+query getPipelineSchedulesQuery($projectPath: ID!, $status: PipelineScheduleStatus) {
project(fullPath: $projectPath) {
id
- pipelineSchedules {
+ pipelineSchedules(status: $status) {
+ count
nodes {
id
description
diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
index 8f77e06c19a..4c06fa321e5 100644
--- a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
@@ -1,9 +1,11 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import PipelineSchedules from './components/pipeline_schedules.vue';
Vue.use(VueApollo);
+Vue.use(GlToast);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
index d83417ab84a..d83417ab84a 100644
--- a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
diff --git a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
index 9fa4b521ebc..9fa4b521ebc 100644
--- a/app/assets/javascripts/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
diff --git a/app/assets/javascripts/runner/admin_runner_show/index.js b/app/assets/javascripts/ci/runner/admin_runner_show/index.js
index ea455416648..ea455416648 100644
--- a/app/assets/javascripts/runner/admin_runner_show/index.js
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/index.js
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
index dbaabb35cde..2915e460085 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue
@@ -4,18 +4,17 @@ import { createAlert } from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
+import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
isSearchFiltered,
-} from 'ee_else_ce/runner/runner_search_utils';
-import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql';
-import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
+} from 'ee_else_ce/ci/runner/runner_search_utils';
+import allRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
-import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
@@ -41,7 +40,6 @@ export default {
components: {
GlLink,
RegistrationDropdown,
- RunnerStackedLayoutBanner,
RunnerFilteredSearchBar,
RunnerList,
RunnerListEmptyState,
@@ -162,8 +160,6 @@ export default {
</script>
<template>
<div>
- <runner-stacked-layout-banner />
-
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
>
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/ci/runner/admin_runners/index.js
index 7bb6cd5689e..c6db7148eb1 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/ci/runner/admin_runners/index.js
@@ -2,7 +2,7 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { visitUrl } from '~/lib/utils/url_utility';
-import { updateOutdatedUrl } from '~/runner/runner_search_utils';
+import { updateOutdatedUrl } from '~/ci/runner/runner_search_utils';
import createDefaultClient from '~/lib/graphql';
import { createLocalState } from '../graphql/list/local_state';
import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
diff --git a/app/assets/javascripts/runner/components/cells/link_cell.vue b/app/assets/javascripts/ci/runner/components/cells/link_cell.vue
index 2843ddbacaf..2843ddbacaf 100644
--- a/app/assets/javascripts/runner/components/cells/link_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/link_cell.vue
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_actions_cell.vue
index 13f520c4edb..13f520c4edb 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_actions_cell.vue
diff --git a/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
index cb43760b2d6..cb43760b2d6 100644
--- a/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_owner_cell.vue
diff --git a/app/assets/javascripts/runner/components/cells/runner_stacked_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue
index e5d49eb7c8e..1e44d5fccc2 100644
--- a/app/assets/javascripts/runner/components/cells/runner_stacked_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue
@@ -26,7 +26,7 @@ export default {
RunnerTags,
RunnerTypeBadge,
RunnerUpgradeStatusIcon: () =>
- import('ee_component/runner/components/runner_upgrade_status_icon.vue'),
+ import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
TooltipOnTruncate,
},
directives: {
diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
index eb98d4ae2fb..67b9b0a266f 100644
--- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
@@ -8,7 +8,7 @@ export default {
components: {
RunnerStatusBadge,
RunnerUpgradeStatusBadge: () =>
- import('ee_component/runner/components/runner_upgrade_status_badge.vue'),
+ import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
RunnerPausedBadge,
},
directives: {
diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
index 1bbbd55089a..1bbbd55089a 100644
--- a/app/assets/javascripts/runner/components/cells/runner_summary_field.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
diff --git a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
index 212ad5fa5a0..212ad5fa5a0 100644
--- a/app/assets/javascripts/runner/components/registration/registration_dropdown.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_dropdown.vue
diff --git a/app/assets/javascripts/runner/components/registration/registration_token.vue b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue
index 6b4e6a929b7..6b4e6a929b7 100644
--- a/app/assets/javascripts/runner/components/registration/registration_token.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_token.vue
diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue
index 667cb0090b3..6740065e860 100644
--- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
+++ b/app/assets/javascripts/ci/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -4,8 +4,8 @@ import { createAlert } from '~/flash';
import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
-import runnersRegistrationTokenResetMutation from '~/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
-import { captureException } from '~/runner/sentry_utils';
+import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/ci/runner/sentry_utils';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
const i18n = {
diff --git a/app/assets/javascripts/runner/components/runner_assigned_item.vue b/app/assets/javascripts/ci/runner/components/runner_assigned_item.vue
index 2fa87bdd776..2fa87bdd776 100644
--- a/app/assets/javascripts/runner/components/runner_assigned_item.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_assigned_item.vue
diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue
index 703da01d9c8..1ec3f8da7c3 100644
--- a/app/assets/javascripts/runner/components/runner_bulk_delete.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_bulk_delete.vue
@@ -117,31 +117,34 @@ export default {
const { errors, deletedIds } = data.bulkRunnerDelete;
if (errors?.length) {
- this.onError(new Error(errors.join(' ')));
- this.$refs.modal.hide();
- return;
+ createAlert({
+ message: s__(
+ 'Runners|An error occurred while deleting. Some runners may not have been deleted.',
+ ),
+ captureError: true,
+ error: new Error(errors.join(' ')),
+ });
}
- this.$emit('deleted', {
- message: this.toastConfirmationMessage(deletedIds.length),
- });
+ if (deletedIds?.length) {
+ this.$emit('deleted', {
+ message: this.toastConfirmationMessage(deletedIds.length),
+ });
- // Clean up
-
- // Remove deleted runners from the cache
- deletedIds.forEach((id) => {
- const cacheId = cache.identify({ __typename: RUNNER_TYPENAME, id });
- cache.evict({ id: cacheId });
- });
- cache.gc();
-
- this.$refs.modal.hide();
+ // Remove deleted runners from the cache
+ deletedIds.forEach((id) => {
+ const cacheId = cache.identify({ __typename: RUNNER_TYPENAME, id });
+ cache.evict({ id: cacheId });
+ });
+ cache.gc();
+ }
},
});
} catch (error) {
this.onError(error);
} finally {
this.isDeleting = false;
+ this.$refs.modal.hide();
}
},
onError(error) {
diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/ci/runner/components/runner_bulk_delete_checkbox.vue
index 75afb7a00bc..75afb7a00bc 100644
--- a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_bulk_delete_checkbox.vue
diff --git a/app/assets/javascripts/runner/components/runner_delete_button.vue b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue
index b4f022a7d14..32d4076b00f 100644
--- a/app/assets/javascripts/runner/components/runner_delete_button.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_delete_button.vue
@@ -1,9 +1,9 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
-import runnerDeleteMutation from '~/runner/graphql/shared/runner_delete.mutation.graphql';
+import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
import { createAlert } from '~/flash';
-import { sprintf } from '~/locale';
-import { captureException } from '~/runner/sentry_utils';
+import { sprintf, s__ } from '~/locale';
+import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants';
import RunnerDeleteModal from './runner_delete_modal.vue';
@@ -122,8 +122,11 @@ export default {
onError(error) {
this.deleting = false;
const { message } = error;
+ const title = sprintf(s__('Runner|Runner %{runnerName} failed to delete'), {
+ runnerName: this.runnerName,
+ });
- createAlert({ message });
+ createAlert({ title, message });
captureException({ error, component: this.$options.name });
},
},
diff --git a/app/assets/javascripts/runner/components/runner_delete_modal.vue b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue
index 8be216a7eb5..8be216a7eb5 100644
--- a/app/assets/javascripts/runner/components/runner_delete_modal.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_delete_modal.vue
diff --git a/app/assets/javascripts/runner/components/runner_detail.vue b/app/assets/javascripts/ci/runner/components/runner_detail.vue
index c260670b517..c260670b517 100644
--- a/app/assets/javascripts/runner/components/runner_detail.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_detail.vue
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/ci/runner/components/runner_details.vue
index 3d72abcd393..6eba8f2e49f 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_details.vue
@@ -18,13 +18,13 @@ export default {
HelpPopover,
RunnerDetail,
RunnerMaintenanceNoteDetail: () =>
- import('ee_component/runner/components/runner_maintenance_note_detail.vue'),
+ import('ee_component/ci/runner/components/runner_maintenance_note_detail.vue'),
RunnerGroups,
RunnerProjects,
RunnerUpgradeStatusBadge: () =>
- import('ee_component/runner/components/runner_upgrade_status_badge.vue'),
+ import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
RunnerUpgradeStatusAlert: () =>
- import('ee_component/runner/components/runner_upgrade_status_alert.vue'),
+ import('ee_component/ci/runner/components/runner_upgrade_status_alert.vue'),
RunnerTags,
TimeAgo,
},
diff --git a/app/assets/javascripts/runner/components/runner_edit_button.vue b/app/assets/javascripts/ci/runner/components/runner_edit_button.vue
index 33e0acaf5c0..33e0acaf5c0 100644
--- a/app/assets/javascripts/runner/components/runner_edit_button.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_edit_button.vue
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
index da59de9a9eb..ee56fea8282 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_filtered_search_bar.vue
@@ -2,7 +2,7 @@
import { cloneDeep } from 'lodash';
import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import { searchValidator } from '~/runner/runner_search_utils';
+import { searchValidator } from '~/ci/runner/runner_search_utils';
import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
diff --git a/app/assets/javascripts/runner/components/runner_groups.vue b/app/assets/javascripts/ci/runner/components/runner_groups.vue
index c3b35bd52a9..c3b35bd52a9 100644
--- a/app/assets/javascripts/runner/components/runner_groups.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_groups.vue
diff --git a/app/assets/javascripts/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue
index 874c234ca4c..874c234ca4c 100644
--- a/app/assets/javascripts/runner/components/runner_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_header.vue
diff --git a/app/assets/javascripts/runner/components/runner_jobs.vue b/app/assets/javascripts/ci/runner/components/runner_jobs.vue
index 9003eba3636..9003eba3636 100644
--- a/app/assets/javascripts/runner/components/runner_jobs.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs.vue
diff --git a/app/assets/javascripts/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index 7817577bab0..efa7909c913 100644
--- a/app/assets/javascripts/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -2,8 +2,9 @@
import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { durationTimeFormatted } from '~/lib/utils/datetime_utility';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
-import RunnerTags from '~/runner/components/runner_tags.vue';
+import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
import LinkCell from './cells/link_cell.vue';
@@ -47,6 +48,14 @@ export default {
commitPath(job) {
return job.commitPath;
},
+ duration(job) {
+ const { duration } = job;
+ return duration ? durationTimeFormatted(duration) : '';
+ },
+ queued(job) {
+ const { queuedDuration } = job;
+ return queuedDuration ? durationTimeFormatted(queuedDuration) : '';
+ },
},
fields: [
tableField({ key: 'status', label: s__('Job|Status') }),
@@ -54,6 +63,8 @@ export default {
tableField({ key: 'project', label: __('Project') }),
tableField({ key: 'commit', label: __('Commit') }),
tableField({ key: 'finished_at', label: s__('Job|Finished at') }),
+ tableField({ key: 'duration', label: s__('Job|Duration') }),
+ tableField({ key: 'queued', label: s__('Job|Queued') }),
tableField({ key: 'tags', label: s__('Runners|Tags') }),
],
};
@@ -84,12 +95,20 @@ export default {
<link-cell :href="commitPath(item)"> {{ commitShortSha(item) }}</link-cell>
</template>
- <template #cell(tags)="{ item = {} }">
- <runner-tags :tag-list="item.tags" />
- </template>
-
<template #cell(finished_at)="{ item = {} }">
<time-ago v-if="item.finishedAt" :time="item.finishedAt" />
</template>
+
+ <template #cell(duration)="{ item = {} }">
+ {{ duration(item) }}
+ </template>
+
+ <template #cell(queued)="{ item = {} }">
+ {{ queued(item) }}
+ </template>
+
+ <template #cell(tags)="{ item = {} }">
+ <runner-tags :tag-list="item.tags" />
+ </template>
</gl-table-lite>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/ci/runner/components/runner_list.vue
index e895537dcdc..e895537dcdc 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list.vue
diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
index e6576c83e69..e6576c83e69 100644
--- a/app/assets/javascripts/runner/components/runner_list_empty_state.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue
diff --git a/app/assets/javascripts/runner/components/runner_membership_toggle.vue b/app/assets/javascripts/ci/runner/components/runner_membership_toggle.vue
index 2b37b1cc797..2b37b1cc797 100644
--- a/app/assets/javascripts/runner/components/runner_membership_toggle.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_membership_toggle.vue
diff --git a/app/assets/javascripts/runner/components/runner_name.vue b/app/assets/javascripts/ci/runner/components/runner_name.vue
index d4ecfd2d776..d4ecfd2d776 100644
--- a/app/assets/javascripts/runner/components/runner_name.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_name.vue
diff --git a/app/assets/javascripts/runner/components/runner_pagination.vue b/app/assets/javascripts/ci/runner/components/runner_pagination.vue
index a5bf3074dd1..a5bf3074dd1 100644
--- a/app/assets/javascripts/runner/components/runner_pagination.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_pagination.vue
diff --git a/app/assets/javascripts/runner/components/runner_pause_button.vue b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue
index 334e5f6023a..2c80518e772 100644
--- a/app/assets/javascripts/runner/components/runner_pause_button.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue
@@ -1,8 +1,8 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import runnerToggleActiveMutation from '~/runner/graphql/shared/runner_toggle_active.mutation.graphql';
+import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import { createAlert } from '~/flash';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME, I18N_RESUME_TOOLTIP } from '../constants';
export default {
diff --git a/app/assets/javascripts/runner/components/runner_paused_badge.vue b/app/assets/javascripts/ci/runner/components/runner_paused_badge.vue
index 00fd84a48d8..00fd84a48d8 100644
--- a/app/assets/javascripts/runner/components/runner_paused_badge.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_paused_badge.vue
diff --git a/app/assets/javascripts/runner/components/runner_projects.vue b/app/assets/javascripts/ci/runner/components/runner_projects.vue
index 84008e8eee8..84008e8eee8 100644
--- a/app/assets/javascripts/runner/components/runner_projects.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_projects.vue
diff --git a/app/assets/javascripts/runner/components/runner_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_status_badge.vue
index d084408781e..d084408781e 100644
--- a/app/assets/javascripts/runner/components/runner_status_badge.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_status_badge.vue
diff --git a/app/assets/javascripts/runner/components/runner_status_popover.vue b/app/assets/javascripts/ci/runner/components/runner_status_popover.vue
index 5b22f7828a1..06174d39a59 100644
--- a/app/assets/javascripts/runner/components/runner_status_popover.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_status_popover.vue
@@ -12,7 +12,7 @@ import {
I18N_STATUS_POPOVER_OFFLINE_DESCRIPTION,
I18N_STATUS_POPOVER_STALE,
I18N_STATUS_POPOVER_STALE_DESCRIPTION,
-} from '~/runner/constants';
+} from '~/ci/runner/constants';
export default {
name: 'RunnerStatusPopover',
diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/ci/runner/components/runner_tag.vue
index 6ad2023a866..6ad2023a866 100644
--- a/app/assets/javascripts/runner/components/runner_tag.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_tag.vue
diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/ci/runner/components/runner_tags.vue
index 38e566f9f53..38e566f9f53 100644
--- a/app/assets/javascripts/runner/components/runner_tags.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_tags.vue
diff --git a/app/assets/javascripts/runner/components/runner_type_badge.vue b/app/assets/javascripts/ci/runner/components/runner_type_badge.vue
index f568f914004..f568f914004 100644
--- a/app/assets/javascripts/runner/components/runner_type_badge.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_type_badge.vue
diff --git a/app/assets/javascripts/runner/components/runner_type_tabs.vue b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
index 6b9e3bf91ad..584236168ac 100644
--- a/app/assets/javascripts/runner/components/runner_type_tabs.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
@@ -1,6 +1,6 @@
<script>
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
-import { searchValidator } from '~/runner/runner_search_utils';
+import { searchValidator } from '~/ci/runner/runner_search_utils';
import { formatNumber } from '~/locale';
import {
INSTANCE_TYPE,
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
index c613e2d2467..a9790d06ca7 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue
@@ -12,11 +12,11 @@ import {
import {
modelToUpdateMutationVariables,
runnerToModel,
-} from 'ee_else_ce/runner/runner_update_form_utils';
+} from 'ee_else_ce/ci/runner/runner_update_form_utils';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { captureException } from '~/runner/sentry_utils';
+import { captureException } from '~/ci/runner/sentry_utils';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
import runnerUpdateMutation from '../graphql/edit/runner_update.mutation.graphql';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
@@ -32,9 +32,9 @@ export default {
GlFormInputGroup,
GlSkeletonLoader,
RunnerMaintenanceNoteField: () =>
- import('ee_component/runner/components/runner_maintenance_note_field.vue'),
+ import('ee_component/ci/runner/components/runner_maintenance_note_field.vue'),
RunnerUpdateCostFactorFields: () =>
- import('ee_component/runner/components/runner_update_cost_factor_fields.vue'),
+ import('ee_component/ci/runner/components/runner_update_cost_factor_fields.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/runner/components/search_tokens/paused_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
index 97ee8ec3eef..97ee8ec3eef 100644
--- a/app/assets/javascripts/runner/components/search_tokens/paused_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
index f5c42d120fb..117a630719e 100644
--- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
@@ -1,5 +1,7 @@
-import { __ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_STATUS,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
I18N_STATUS_ONLINE,
@@ -22,7 +24,7 @@ const options = [
export const statusTokenConfig = {
icon: 'status',
- title: __('Status'),
+ title: TOKEN_TITLE_STATUS,
type: PARAM_KEY_STATUS,
token: BaseToken,
unique: true,
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
index 6e7c41885f8..6e7c41885f8 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token.vue
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
index fdeba714385..fdeba714385 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
diff --git a/app/assets/javascripts/runner/components/search_tokens/upgrade_status_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/upgrade_status_token_config.js
index 17ee7073360..17ee7073360 100644
--- a/app/assets/javascripts/runner/components/search_tokens/upgrade_status_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/upgrade_status_token_config.js
diff --git a/app/assets/javascripts/runner/components/stat/runner_count.vue b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
index 37c6f922f9a..4ad9259f59d 100644
--- a/app/assets/javascripts/runner/components/stat/runner_count.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
@@ -1,7 +1,7 @@
<script>
import { fetchPolicies } from '~/lib/graphql';
-import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_count.query.graphql';
-import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
+import allRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/all_runners_count.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
import { captureException } from '../../sentry_utils';
import { INSTANCE_TYPE, GROUP_TYPE } from '../../constants';
diff --git a/app/assets/javascripts/runner/components/stat/runner_single_stat.vue b/app/assets/javascripts/ci/runner/components/stat/runner_single_stat.vue
index ae732b052ac..ae732b052ac 100644
--- a/app/assets/javascripts/runner/components/stat/runner_single_stat.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_single_stat.vue
diff --git a/app/assets/javascripts/runner/components/stat/runner_stats.vue b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
index 4df59f5a0c9..3965e5551f1 100644
--- a/app/assets/javascripts/runner/components/stat/runner_stats.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
@@ -1,5 +1,5 @@
<script>
-import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
+import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
import {
I18N_STATUS_ONLINE,
I18N_STATUS_OFFLINE,
@@ -13,7 +13,7 @@ export default {
components: {
RunnerSingleStat,
RunnerUpgradeStatusStats: () =>
- import('ee_component/runner/components/stat/runner_upgrade_status_stats.vue'),
+ import('ee_component/ci/runner/components/stat/runner_upgrade_status_stats.vue'),
},
props: {
scope: {
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index dfc5f0c4152..dfc5f0c4152 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields.fragment.graphql
index b732d587d70..b732d587d70 100644
--- a/app/assets/javascripts/runner/graphql/edit/runner_fields.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields.fragment.graphql
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
index 29abddf84f5..29abddf84f5 100644
--- a/app/assets/javascripts/runner/graphql/edit/runner_fields_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_form.query.graphql
index 0bf66c223fc..5599c147c56 100644
--- a/app/assets/javascripts/runner/graphql/edit/runner_form.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_form.query.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/runner/graphql/edit/runner_fields.fragment.graphql"
+#import "ee_else_ce/ci/runner/graphql/edit/runner_fields.fragment.graphql"
query getRunnerForm($id: CiRunnerID!) {
runner(id: $id) {
diff --git a/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_update.mutation.graphql
index 8694a51b5a4..9469078c317 100644
--- a/app/assets/javascripts/runner/graphql/edit/runner_update.mutation.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_update.mutation.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/runner/graphql/edit/runner_fields.fragment.graphql"
+#import "ee_else_ce/ci/runner/graphql/edit/runner_fields.fragment.graphql"
# Mutation for updates from the runner form, loads
# attributes shown in the runner details.
diff --git a/app/assets/javascripts/runner/graphql/list/all_runners.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
index 1160596aff3..15401c25c64 100644
--- a/app/assets/javascripts/runner/graphql/list/all_runners.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners.query.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/list/all_runners_connection.fragment.graphql"
+#import "~/ci/runner/graphql/list/all_runners_connection.fragment.graphql"
query getAllRunners(
$before: String
diff --git a/app/assets/javascripts/runner/graphql/list/all_runners_connection.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners_connection.fragment.graphql
index 4440b8e98da..39d79df02e7 100644
--- a/app/assets/javascripts/runner/graphql/list/all_runners_connection.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners_connection.fragment.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql"
+#import "ee_else_ce/ci/runner/graphql/list/list_item.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
fragment AllRunnersConnection on CiRunnerConnection {
diff --git a/app/assets/javascripts/runner/graphql/list/all_runners_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
index 82591b88d3e..82591b88d3e 100644
--- a/app/assets/javascripts/runner/graphql/list/all_runners_count.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/all_runners_count.query.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/bulk_runner_delete.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql
index b73c016b1de..b73c016b1de 100644
--- a/app/assets/javascripts/runner/graphql/list/bulk_runner_delete.mutation.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/bulk_runner_delete.mutation.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/checked_runner_ids.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/checked_runner_ids.query.graphql
index c01f1edb451..c01f1edb451 100644
--- a/app/assets/javascripts/runner/graphql/list/checked_runner_ids.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/checked_runner_ids.query.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/group_runner_connection.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/group_runner_connection.fragment.graphql
index baef16a4b41..53be8fdc613 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runner_connection.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/group_runner_connection.fragment.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/runner/graphql/list/list_item.fragment.graphql"
+#import "ee_else_ce/ci/runner/graphql/list/list_item.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
fragment GroupRunnerConnection on CiRunnerConnection {
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/group_runners.query.graphql
index 95f9dd1beb9..08fd8974826 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/group_runners.query.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/list/group_runner_connection.fragment.graphql"
+#import "~/ci/runner/graphql/list/group_runner_connection.fragment.graphql"
query getGroupRunners(
$groupFullPath: ID!
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/group_runners_count.query.graphql
index e88a2c2e7e6..e88a2c2e7e6 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/group_runners_count.query.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item.fragment.graphql
index 19a5a48ea75..19a5a48ea75 100644
--- a/app/assets/javascripts/runner/graphql/list/list_item.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/list_item.fragment.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
index 0dff011daaa..0dff011daaa 100644
--- a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/ci/runner/graphql/list/local_state.js
index e0477c660b4..ab53bfdbd5b 100644
--- a/app/assets/javascripts/runner/graphql/list/local_state.js
+++ b/app/assets/javascripts/ci/runner/graphql/list/local_state.js
@@ -8,7 +8,7 @@ import typeDefs from './typedefs.graphql';
* Usage:
*
* ```
- * import { createLocalState } from '~/runner/graphql/list/local_state';
+ * import { createLocalState } from '~/ci/runner/graphql/list/local_state';
*
* // initialize local state
* const { cacheConfig, typeDefs, localMutations } = createLocalState();
diff --git a/app/assets/javascripts/runner/graphql/list/runners_registration_token_reset.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql
index 9c2797732ad..9c2797732ad 100644
--- a/app/assets/javascripts/runner/graphql/list/runners_registration_token_reset.mutation.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql
diff --git a/app/assets/javascripts/runner/graphql/list/typedefs.graphql b/app/assets/javascripts/ci/runner/graphql/list/typedefs.graphql
index 24e9e20cc8c..24e9e20cc8c 100644
--- a/app/assets/javascripts/runner/graphql/list/typedefs.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/typedefs.graphql
diff --git a/app/assets/javascripts/runner/graphql/shared/runner_delete.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/shared/runner_delete.mutation.graphql
index d580ea2785e..d580ea2785e 100644
--- a/app/assets/javascripts/runner/graphql/shared/runner_delete.mutation.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/shared/runner_delete.mutation.graphql
diff --git a/app/assets/javascripts/runner/graphql/shared/runner_toggle_active.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql
index 9b15570dbc0..9b15570dbc0 100644
--- a/app/assets/javascripts/runner/graphql/shared/runner_toggle_active.mutation.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql
diff --git a/app/assets/javascripts/runner/graphql/show/runner.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner.query.graphql
index dec434b43a5..6375b4f35a4 100644
--- a/app/assets/javascripts/runner/graphql/show/runner.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner.query.graphql
@@ -1,4 +1,4 @@
-#import "ee_else_ce/runner/graphql/show/runner_details.fragment.graphql"
+#import "ee_else_ce/ci/runner/graphql/show/runner_details.fragment.graphql"
query getRunner($id: CiRunnerID!) {
runner(id: $id) {
diff --git a/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details.fragment.graphql
index 2449ee0fc0f..2449ee0fc0f 100644
--- a/app/assets/javascripts/runner/graphql/show/runner_details.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details.fragment.graphql
diff --git a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
index b5689ff7687..b5689ff7687 100644
--- a/app/assets/javascripts/runner/graphql/show/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql
diff --git a/app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
index 14585e62bf2..edfc22f644b 100644
--- a/app/assets/javascripts/runner/graphql/show/runner_jobs.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
@@ -25,8 +25,10 @@ query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String,
}
shortSha
commitPath
- tags
finishedAt
+ duration
+ queuedDuration
+ tags
}
pageInfo {
...PageInfo
diff --git a/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_projects.query.graphql
index e42648b3079..e42648b3079 100644
--- a/app/assets/javascripts/runner/graphql/show/runner_projects.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_projects.query.graphql
diff --git a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue
index 75138b1bd81..75138b1bd81 100644
--- a/app/assets/javascripts/runner/group_runner_show/group_runner_show_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runner_show/group_runner_show_app.vue
diff --git a/app/assets/javascripts/runner/group_runner_show/index.js b/app/assets/javascripts/ci/runner/group_runner_show/index.js
index e75f337b38e..e75f337b38e 100644
--- a/app/assets/javascripts/runner/group_runner_show/index.js
+++ b/app/assets/javascripts/ci/runner/group_runner_show/index.js
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index 7f56d895682..91c22923075 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -3,19 +3,18 @@ import { GlLink } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { updateHistory } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
-import { upgradeStatusTokenConfig } from 'ee_else_ce/runner/components/search_tokens/upgrade_status_token_config';
+import { upgradeStatusTokenConfig } from 'ee_else_ce/ci/runner/components/search_tokens/upgrade_status_token_config';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
fromSearchToVariables,
isSearchFiltered,
-} from 'ee_else_ce/runner/runner_search_utils';
+} from 'ee_else_ce/ci/runner/runner_search_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
-import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners_count.query.graphql';
+import groupRunnersQuery from 'ee_else_ce/ci/runner/graphql/list/group_runners.query.graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
-import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
@@ -43,7 +42,6 @@ export default {
components: {
GlLink,
RegistrationDropdown,
- RunnerStackedLayoutBanner,
RunnerFilteredSearchBar,
RunnerList,
RunnerListEmptyState,
@@ -201,8 +199,6 @@ export default {
<template>
<div>
- <runner-stacked-layout-banner />
-
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
ref="runner-type-tabs"
@@ -250,7 +246,12 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted">
+ <runner-list
+ :runners="runners.items"
+ :checkable="true"
+ :loading="runnersLoading"
+ @deleted="onDeleted"
+ >
<template #runner-name="{ runner }">
<gl-link :href="webUrl(runner)">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/ci/runner/group_runners/index.js
index 0e7efd2b8a1..0e7efd2b8a1 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/ci/runner/group_runners/index.js
diff --git a/app/assets/javascripts/runner/local_storage_alert/constants.js b/app/assets/javascripts/ci/runner/local_storage_alert/constants.js
index 69b7418f898..69b7418f898 100644
--- a/app/assets/javascripts/runner/local_storage_alert/constants.js
+++ b/app/assets/javascripts/ci/runner/local_storage_alert/constants.js
diff --git a/app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js b/app/assets/javascripts/ci/runner/local_storage_alert/save_alert_to_local_storage.js
index ca7c627459a..ca7c627459a 100644
--- a/app/assets/javascripts/runner/local_storage_alert/save_alert_to_local_storage.js
+++ b/app/assets/javascripts/ci/runner/local_storage_alert/save_alert_to_local_storage.js
diff --git a/app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js b/app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js
index d768a06494a..d768a06494a 100644
--- a/app/assets/javascripts/runner/local_storage_alert/show_alert_from_local_storage.js
+++ b/app/assets/javascripts/ci/runner/local_storage_alert/show_alert_from_local_storage.js
diff --git a/app/assets/javascripts/runner/runner_edit/index.js b/app/assets/javascripts/ci/runner/runner_edit/index.js
index 5b2ddb8f68e..5b2ddb8f68e 100644
--- a/app/assets/javascripts/runner/runner_edit/index.js
+++ b/app/assets/javascripts/ci/runner/runner_edit/index.js
diff --git a/app/assets/javascripts/runner/runner_edit/runner_edit_app.vue b/app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue
index 879162916a9..879162916a9 100644
--- a/app/assets/javascripts/runner/runner_edit/runner_edit_app.vue
+++ b/app/assets/javascripts/ci/runner/runner_edit/runner_edit_app.vue
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index adc832b0600..adc832b0600 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
diff --git a/app/assets/javascripts/runner/runner_update_form_utils.js b/app/assets/javascripts/ci/runner/runner_update_form_utils.js
index 3b519fa7d71..3b519fa7d71 100644
--- a/app/assets/javascripts/runner/runner_update_form_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_update_form_utils.js
diff --git a/app/assets/javascripts/runner/sentry_utils.js b/app/assets/javascripts/ci/runner/sentry_utils.js
index 29de1f9adae..29de1f9adae 100644
--- a/app/assets/javascripts/runner/sentry_utils.js
+++ b/app/assets/javascripts/ci/runner/sentry_utils.js
diff --git a/app/assets/javascripts/runner/utils.js b/app/assets/javascripts/ci/runner/utils.js
index 1ca0a9e86b5..1ca0a9e86b5 100644
--- a/app/assets/javascripts/runner/utils.js
+++ b/app/assets/javascripts/ci/runner/utils.js
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
index 8d891ff1746..719696f682e 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
@@ -1,143 +1,36 @@
<script>
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
-import { reportMessageToSentry } from '../utils';
+import { ADD_MUTATION_ACTION, DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION } from '../constants';
import getAdminVariables from '../graphql/queries/variables.query.graphql';
-import {
- ADD_MUTATION_ACTION,
- DELETE_MUTATION_ACTION,
- UPDATE_MUTATION_ACTION,
- genericMutationErrorText,
- variableFetchErrorText,
-} from '../constants';
import addAdminVariable from '../graphql/mutations/admin_add_variable.mutation.graphql';
import deleteAdminVariable from '../graphql/mutations/admin_delete_variable.mutation.graphql';
import updateAdminVariable from '../graphql/mutations/admin_update_variable.mutation.graphql';
-import CiVariableSettings from './ci_variable_settings.vue';
+import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
- CiVariableSettings,
+ CiVariableShared,
},
- inject: ['endpoint'],
- data() {
- return {
- adminVariables: [],
- hasNextPage: false,
- isInitialLoading: true,
- isLoadingMoreItems: false,
- loadingCounter: 0,
- pageInfo: {},
- };
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addAdminVariable,
+ [UPDATE_MUTATION_ACTION]: updateAdminVariable,
+ [DELETE_MUTATION_ACTION]: deleteAdminVariable,
},
- apollo: {
- adminVariables: {
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.ciVariables,
query: getAdminVariables,
- update(data) {
- return data?.ciVariables?.nodes || [];
- },
- result({ data }) {
- this.pageInfo = data?.ciVariables?.pageInfo || this.pageInfo;
- this.hasNextPage = this.pageInfo?.hasNextPage || false;
-
- // Because graphQL has a limit of 100 items,
- // we batch load all the variables by making successive queries
- // to keep the same UX. As a safeguard, we make sure that we cannot go over
- // 20 consecutive API calls, which means 2000 variables loaded maximum.
- if (!this.hasNextPage) {
- this.isLoadingMoreItems = false;
- } else if (this.loadingCounter < 20) {
- this.hasNextPage = false;
- this.fetchMoreVariables();
- this.loadingCounter += 1;
- } else {
- createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
- }
- },
- error() {
- this.isLoadingMoreItems = false;
- this.hasNextPage = false;
- createAlert({ message: variableFetchErrorText });
- },
- watchLoading(flag) {
- if (!flag) {
- this.isInitialLoading = false;
- }
- },
- },
- },
- computed: {
- isLoading() {
- return (
- (this.$apollo.queries.adminVariables.loading && this.isInitialLoading) ||
- this.isLoadingMoreItems
- );
},
},
- methods: {
- addVariable(variable) {
- this.variableMutation(ADD_MUTATION_ACTION, variable);
- },
- deleteVariable(variable) {
- this.variableMutation(DELETE_MUTATION_ACTION, variable);
- },
- fetchMoreVariables() {
- this.isLoadingMoreItems = true;
-
- this.$apollo.queries.adminVariables.fetchMore({
- variables: {
- after: this.pageInfo.endCursor,
- },
- });
- },
- updateVariable(variable) {
- this.variableMutation(UPDATE_MUTATION_ACTION, variable);
- },
- async variableMutation(mutationAction, variable) {
- try {
- const currentMutation = this.$options.mutationData[mutationAction];
- const { data } = await this.$apollo.mutate({
- mutation: currentMutation.action,
- variables: {
- endpoint: this.endpoint,
- variable,
- },
- });
-
- if (data[currentMutation.name]?.errors?.length) {
- const { errors } = data[currentMutation.name];
- createAlert({ message: errors[0] });
- } else {
- // The writing to cache for admin variable is not working
- // because there is no ID in the cache at the top level.
- // We therefore need to manually refetch.
- this.$apollo.queries.adminVariables.refetch();
- }
- } catch {
- createAlert({ message: genericMutationErrorText });
- }
- },
- },
- componentName: 'InstanceVariables',
- i18n: {
- tooManyCallsError: __('Maximum number of variables loaded (2000)'),
- },
- mutationData: {
- [ADD_MUTATION_ACTION]: { action: addAdminVariable, name: 'addAdminVariable' },
- [UPDATE_MUTATION_ACTION]: { action: updateAdminVariable, name: 'updateAdminVariable' },
- [DELETE_MUTATION_ACTION]: { action: deleteAdminVariable, name: 'deleteAdminVariable' },
- },
};
</script>
<template>
- <ci-variable-settings
+ <ci-variable-shared
:are-scoped-variables-available="false"
- :is-loading="isLoading"
- :variables="adminVariables"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @update-variable="updateVariable"
+ component-name="InstanceVariables"
+ :hide-environment-scope="true"
+ :mutation-data="$options.mutationData"
+ :refetch-after-mutation="true"
+ :query-data="$options.queryData"
/>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
index 4af696b8dab..c8f5ac1736d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -1,143 +1,53 @@
<script>
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportMessageToSentry } from '../utils';
-import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_GROUP_TYPE,
UPDATE_MUTATION_ACTION,
- genericMutationErrorText,
- variableFetchErrorText,
} from '../constants';
+import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import addGroupVariable from '../graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '../graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '../graphql/mutations/group_update_variable.mutation.graphql';
-import CiVariableSettings from './ci_variable_settings.vue';
+import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
- CiVariableSettings,
+ CiVariableShared,
},
mixins: [glFeatureFlagsMixin()],
- inject: ['endpoint', 'groupPath', 'groupId'],
- data() {
- return {
- groupVariables: [],
- hasNextPage: false,
- isLoadingMoreItems: false,
- loadingCounter: 0,
- pageInfo: {},
- };
- },
- apollo: {
- groupVariables: {
- query: getGroupVariables,
- variables() {
- return {
- fullPath: this.groupPath,
- };
- },
- update(data) {
- return data?.group?.ciVariables?.nodes || [];
- },
- result({ data }) {
- this.pageInfo = data?.group?.ciVariables?.pageInfo || this.pageInfo;
- this.hasNextPage = this.pageInfo?.hasNextPage || false;
- // Because graphQL has a limit of 100 items,
- // we batch load all the variables by making successive queries
- // to keep the same UX. As a safeguard, we make sure that we cannot go over
- // 20 consecutive API calls, which means 2000 variables loaded maximum.
- if (!this.hasNextPage) {
- this.isLoadingMoreItems = false;
- } else if (this.loadingCounter < 20) {
- this.hasNextPage = false;
- this.fetchMoreVariables();
- this.loadingCounter += 1;
- } else {
- createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
- }
- },
- error() {
- this.isLoadingMoreItems = false;
- this.hasNextPage = false;
- createAlert({ message: variableFetchErrorText });
- },
- },
- },
+ inject: ['groupPath', 'groupId'],
computed: {
areScopedVariablesAvailable() {
return this.glFeatures.groupScopedCiVariables;
},
- isLoading() {
- return this.$apollo.queries.groupVariables.loading || this.isLoadingMoreItems;
- },
- },
- methods: {
- addVariable(variable) {
- this.variableMutation(ADD_MUTATION_ACTION, variable);
- },
- deleteVariable(variable) {
- this.variableMutation(DELETE_MUTATION_ACTION, variable);
- },
- fetchMoreVariables() {
- this.isLoadingMoreItems = true;
-
- this.$apollo.queries.groupVariables.fetchMore({
- variables: {
- fullPath: this.groupPath,
- after: this.pageInfo.endCursor,
- },
- });
- },
- updateVariable(variable) {
- this.variableMutation(UPDATE_MUTATION_ACTION, variable);
- },
- async variableMutation(mutationAction, variable) {
- try {
- const currentMutation = this.$options.mutationData[mutationAction];
- const { data } = await this.$apollo.mutate({
- mutation: currentMutation.action,
- variables: {
- endpoint: this.endpoint,
- fullPath: this.groupPath,
- groupId: convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId),
- variable,
- },
- });
-
- if (data[currentMutation.name]?.errors?.length) {
- const { errors } = data[currentMutation.name];
- createAlert({ message: errors[0] });
- }
- } catch {
- createAlert({ message: genericMutationErrorText });
- }
+ graphqlId() {
+ return convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId);
},
},
- componentName: 'GroupVariables',
- i18n: {
- tooManyCallsError: __('Maximum number of variables loaded (2000)'),
- },
mutationData: {
- [ADD_MUTATION_ACTION]: { action: addGroupVariable, name: 'addGroupVariable' },
- [UPDATE_MUTATION_ACTION]: { action: updateGroupVariable, name: 'updateGroupVariable' },
- [DELETE_MUTATION_ACTION]: { action: deleteGroupVariable, name: 'deleteGroupVariable' },
+ [ADD_MUTATION_ACTION]: addGroupVariable,
+ [UPDATE_MUTATION_ACTION]: updateGroupVariable,
+ [DELETE_MUTATION_ACTION]: deleteGroupVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.group?.ciVariables,
+ query: getGroupVariables,
+ },
},
};
</script>
<template>
- <ci-variable-settings
+ <ci-variable-shared
+ :id="graphqlId"
:are-scoped-variables-available="areScopedVariablesAvailable"
- :is-loading="isLoading"
- :variables="groupVariables"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @update-variable="updateVariable"
+ component-name="GroupVariables"
+ :full-path="groupPath"
+ :mutation-data="$options.mutationData"
+ :query-data="$options.queryData"
/>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
index 6bd549817f8..2c4818e20c1 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
@@ -1,160 +1,55 @@
<script>
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
-import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
-import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_PROJECT_TYPE,
UPDATE_MUTATION_ACTION,
- environmentFetchErrorText,
- genericMutationErrorText,
- variableFetchErrorText,
} from '../constants';
+import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
import addProjectVariable from '../graphql/mutations/project_add_variable.mutation.graphql';
import deleteProjectVariable from '../graphql/mutations/project_delete_variable.mutation.graphql';
import updateProjectVariable from '../graphql/mutations/project_update_variable.mutation.graphql';
-import CiVariableSettings from './ci_variable_settings.vue';
+import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
- CiVariableSettings,
- },
- inject: ['endpoint', 'projectFullPath', 'projectId'],
- data() {
- return {
- hasNextPage: false,
- isLoadingMoreItems: false,
- loadingCounter: 0,
- pageInfo: {},
- projectEnvironments: [],
- projectVariables: [],
- };
- },
- apollo: {
- projectEnvironments: {
- query: getProjectEnvironments,
- variables() {
- return {
- fullPath: this.projectFullPath,
- };
- },
- update(data) {
- return mapEnvironmentNames(data?.project?.environments?.nodes);
- },
- error() {
- createAlert({ message: environmentFetchErrorText });
- },
- },
- projectVariables: {
- query: getProjectVariables,
- variables() {
- return {
- after: null,
- fullPath: this.projectFullPath,
- };
- },
- update(data) {
- return data?.project?.ciVariables?.nodes || [];
- },
- result({ data }) {
- this.pageInfo = data?.project?.ciVariables?.pageInfo || this.pageInfo;
- this.hasNextPage = this.pageInfo?.hasNextPage || false;
- // Because graphQL has a limit of 100 items,
- // we batch load all the variables by making successive queries
- // to keep the same UX. As a safeguard, we make sure that we cannot go over
- // 20 consecutive API calls, which means 2000 variables loaded maximum.
- if (!this.hasNextPage) {
- this.isLoadingMoreItems = false;
- } else if (this.loadingCounter < 20) {
- this.hasNextPage = false;
- this.fetchMoreVariables();
- this.loadingCounter += 1;
- } else {
- createAlert({ message: this.$options.tooManyCallsError });
- reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
- }
- },
- error() {
- this.isLoadingMoreItems = false;
- this.hasNextPage = false;
- createAlert({ message: variableFetchErrorText });
- },
- },
+ CiVariableShared,
},
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['projectFullPath', 'projectId'],
computed: {
- isLoading() {
- return (
- this.$apollo.queries.projectVariables.loading ||
- this.$apollo.queries.projectEnvironments.loading ||
- this.isLoadingMoreItems
- );
+ graphqlId() {
+ return convertToGraphQLId(GRAPHQL_PROJECT_TYPE, this.projectId);
},
},
- methods: {
- addVariable(variable) {
- this.variableMutation(ADD_MUTATION_ACTION, variable);
- },
- deleteVariable(variable) {
- this.variableMutation(DELETE_MUTATION_ACTION, variable);
- },
- fetchMoreVariables() {
- this.isLoadingMoreItems = true;
-
- this.$apollo.queries.projectVariables.fetchMore({
- variables: {
- fullPath: this.projectFullPath,
- after: this.pageInfo.endCursor,
- },
- });
- },
- updateVariable(variable) {
- this.variableMutation(UPDATE_MUTATION_ACTION, variable);
+ mutationData: {
+ [ADD_MUTATION_ACTION]: addProjectVariable,
+ [UPDATE_MUTATION_ACTION]: updateProjectVariable,
+ [DELETE_MUTATION_ACTION]: deleteProjectVariable,
+ },
+ queryData: {
+ ciVariables: {
+ lookup: (data) => data?.project?.ciVariables,
+ query: getProjectVariables,
},
- async variableMutation(mutationAction, variable) {
- try {
- const currentMutation = this.$options.mutationData[mutationAction];
- const { data } = await this.$apollo.mutate({
- mutation: currentMutation.action,
- variables: {
- endpoint: this.endpoint,
- fullPath: this.projectFullPath,
- projectId: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, this.projectId),
- variable,
- },
- });
- if (data[currentMutation.name]?.errors?.length) {
- const { errors } = data[currentMutation.name];
- createAlert({ message: errors[0] });
- }
- } catch {
- createAlert({ message: genericMutationErrorText });
- }
+ environments: {
+ lookup: (data) => data?.project?.environments,
+ query: getProjectEnvironments,
},
},
- componentName: 'ProjectVariables',
- i18n: {
- tooManyCallsError: __('Maximum number of variables loaded (2000)'),
- },
- mutationData: {
- [ADD_MUTATION_ACTION]: { action: addProjectVariable, name: 'addProjectVariable' },
- [UPDATE_MUTATION_ACTION]: { action: updateProjectVariable, name: 'updateProjectVariable' },
- [DELETE_MUTATION_ACTION]: { action: deleteProjectVariable, name: 'deleteProjectVariable' },
- },
};
</script>
<template>
- <ci-variable-settings
+ <ci-variable-shared
+ :id="graphqlId"
:are-scoped-variables-available="true"
- :environments="projectEnvironments"
- :is-loading="isLoading"
- :variables="projectVariables"
- @add-variable="addVariable"
- @delete-variable="deleteVariable"
- @update-variable="updateVariable"
+ component-name="ProjectVariables"
+ :full-path="projectFullPath"
+ :mutation-data="$options.mutationData"
+ :query-data="$options.queryData"
/>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 56c1804910a..94f8cb9e906 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -86,6 +86,11 @@ export default {
required: false,
default: () => [],
},
+ hideEnvironmentScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
mode: {
type: String,
required: true,
@@ -293,10 +298,11 @@ export default {
v-model="variable.value"
:state="variableValidationState"
rows="3"
- max-rows="6"
+ max-rows="10"
data-testid="pipeline-form-ci-variable-value"
data-qa-selector="ci_variable_value_field"
class="gl-font-monospace!"
+ spellcheck="false"
/>
</gl-form-group>
@@ -309,33 +315,35 @@ export default {
/>
</gl-form-group>
- <gl-form-group
- label-for="ci-variable-env"
- class="gl-w-half"
- data-testid="environment-scope"
- >
- <template #label>
- {{ __('Environment scope') }}
- <gl-link
- :title="$options.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
- </template>
- <ci-environments-dropdown
- v-if="areScopedVariablesAvailable"
- class="gl-w-full"
- :selected-environment-scope="variable.environmentScope"
- :environments="joinedEnvironments"
- @select-environment="setEnvironmentScope"
- @create-environment-scope="createEnvironmentScope"
- />
+ <template v-if="!hideEnvironmentScope">
+ <gl-form-group
+ label-for="ci-variable-env"
+ class="gl-w-half"
+ data-testid="environment-scope"
+ >
+ <template #label>
+ {{ __('Environment scope') }}
+ <gl-link
+ :title="$options.environmentScopeLinkTitle"
+ :href="environmentScopeLink"
+ target="_blank"
+ data-testid="environment-scope-link"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </template>
+ <ci-environments-dropdown
+ v-if="areScopedVariablesAvailable"
+ class="gl-w-full"
+ :selected-environment-scope="variable.environmentScope"
+ :environments="joinedEnvironments"
+ @select-environment="setEnvironmentScope"
+ @create-environment-scope="createEnvironmentScope"
+ />
- <gl-form-input v-else :value="$options.defaultScope" class="gl-w-full" readonly />
- </gl-form-group>
+ <gl-form-input v-else :value="$options.defaultScope" class="gl-w-full" readonly />
+ </gl-form-group>
+ </template>
</div>
<gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
deleted file mode 100644
index 605da5d9352..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_popover.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui';
-
-export default {
- maxTextLength: 95,
- components: {
- GlPopover,
- GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- target: {
- type: String,
- required: true,
- },
- value: {
- type: String,
- required: true,
- },
- tooltipText: {
- type: String,
- required: true,
- },
- },
- computed: {
- displayValue() {
- if (this.value.length > this.$options.maxTextLength) {
- return `${this.value.substring(0, this.$options.maxTextLength)}...`;
- }
- return this.value;
- },
- },
-};
-</script>
-
-<template>
- <div id="popover-container">
- <gl-popover :target="target" placement="top" container="popover-container">
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-word-break-all"
- >
- <div class="ci-popover-value gl-pr-3">
- {{ displayValue }}
- </div>
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- :title="tooltipText"
- :data-clipboard-text="value"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </gl-popover>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index 81e3a983ea3..94fd6c3892c 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -19,6 +19,11 @@ export default {
required: false,
default: () => [],
},
+ hideEnvironmentScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isLoading: {
type: Boolean,
required: false,
@@ -78,6 +83,7 @@ export default {
v-if="showModal"
:are-scoped-variables-available="areScopedVariablesAvailable"
:environments="environments"
+ :hide-environment-scope="hideEnvironmentScope"
:variables="variables"
:mode="mode"
:selected-variable="selectedVariable"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
new file mode 100644
index 00000000000..7ee250cea98
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
@@ -0,0 +1,232 @@
+<script>
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ environmentFetchErrorText,
+ genericMutationErrorText,
+ variableFetchErrorText,
+} from '../constants';
+import CiVariableSettings from './ci_variable_settings.vue';
+
+export default {
+ components: {
+ CiVariableSettings,
+ },
+ inject: ['endpoint'],
+ props: {
+ areScopedVariablesAvailable: {
+ required: true,
+ type: Boolean,
+ },
+ componentName: {
+ required: true,
+ type: String,
+ },
+ fullPath: {
+ required: false,
+ type: String,
+ default: null,
+ },
+ hideEnvironmentScope: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ id: {
+ required: false,
+ type: String,
+ default: null,
+ },
+ mutationData: {
+ required: true,
+ type: Object,
+ validator: (obj) => {
+ const hasValidKeys = Object.keys(obj).includes(
+ ADD_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ );
+
+ const hasValidValues = Object.values(obj).reduce((acc, val) => {
+ return acc && typeof val === 'object';
+ }, true);
+
+ return hasValidKeys && hasValidValues;
+ },
+ },
+ refetchAfterMutation: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
+ queryData: {
+ required: true,
+ type: Object,
+ validator: (obj) => {
+ const { ciVariables, environments } = obj;
+ const hasCiVariablesKey = Boolean(ciVariables);
+ let hasCorrectEnvData = true;
+
+ const hasCorrectVariablesData =
+ typeof ciVariables?.lookup === 'function' && typeof ciVariables.query === 'object';
+
+ if (environments) {
+ hasCorrectEnvData =
+ typeof environments?.lookup === 'function' && typeof environments.query === 'object';
+ }
+
+ return hasCiVariablesKey && hasCorrectVariablesData && hasCorrectEnvData;
+ },
+ },
+ },
+ data() {
+ return {
+ ciVariables: [],
+ hasNextPage: false,
+ isInitialLoading: true,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
+ };
+ },
+ apollo: {
+ ciVariables: {
+ query() {
+ return this.queryData.ciVariables.query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath || undefined,
+ };
+ },
+ update(data) {
+ return this.queryData.ciVariables.lookup(data)?.nodes || [];
+ },
+ result({ data }) {
+ this.pageInfo = this.queryData.ciVariables.lookup(data)?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
+ error() {
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
+ },
+ watchLoading(flag) {
+ if (!flag) {
+ this.isInitialLoading = false;
+ }
+ },
+ },
+ environments: {
+ query() {
+ return this.queryData?.environments?.query || {};
+ },
+ skip() {
+ return !this.queryData?.environments?.query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return mapEnvironmentNames(this.queryData.environments.lookup(data)?.nodes);
+ },
+ error() {
+ createAlert({ message: environmentFetchErrorText });
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return (
+ (this.$apollo.queries.ciVariables.loading && this.isInitialLoading) ||
+ this.$apollo.queries.environments.loading ||
+ this.isLoadingMoreItems
+ );
+ },
+ },
+ methods: {
+ addVariable(variable) {
+ this.variableMutation(ADD_MUTATION_ACTION, variable);
+ },
+ deleteVariable(variable) {
+ this.variableMutation(DELETE_MUTATION_ACTION, variable);
+ },
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.ciVariables.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
+ updateVariable(variable) {
+ this.variableMutation(UPDATE_MUTATION_ACTION, variable);
+ },
+ async variableMutation(mutationAction, variable) {
+ try {
+ const currentMutation = this.mutationData[mutationAction];
+
+ const { data } = await this.$apollo.mutate({
+ mutation: currentMutation,
+ variables: {
+ endpoint: this.endpoint,
+ fullPath: this.fullPath || undefined,
+ id: this.id || undefined,
+ variable,
+ },
+ });
+
+ if (data.ciVariableMutation?.errors?.length) {
+ const { errors } = data.ciVariableMutation;
+ createAlert({ message: errors[0] });
+ } else if (this.refetchAfterMutation) {
+ // The writing to cache for admin variable is not working
+ // because there is no ID in the cache at the top level.
+ // We therefore need to manually refetch.
+ this.$apollo.queries.ciVariables.refetch();
+ }
+ } catch (e) {
+ createAlert({ message: genericMutationErrorText });
+ }
+ },
+ },
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
+};
+</script>
+
+<template>
+ <ci-variable-settings
+ :are-scoped-variables-available="areScopedVariablesAvailable"
+ :hide-environment-scope="hideEnvironmentScope"
+ :is-loading="isLoading"
+ :variables="ciVariables"
+ :environments="environments"
+ @add-variable="addVariable"
+ @delete-variable="deleteVariable"
+ @update-variable="updateVariable"
+ />
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 959ef6864fb..3cdcb68e919 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -1,71 +1,49 @@
<script>
-import {
- GlButton,
- GlIcon,
- GlLoadingIcon,
- GlModalDirective,
- GlTable,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlModalDirective, GlTable, GlTooltipDirective } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { ADD_CI_VARIABLE_MODAL_ID, variableText } from '../constants';
import { convertEnvironmentScope } from '../utils';
-import CiVariablePopover from './ci_variable_popover.vue';
export default {
modalId: ADD_CI_VARIABLE_MODAL_ID,
- trueIcon: 'mobile-issue-close',
- falseIcon: 'close',
- iconSize: 16,
fields: [
{
key: 'variableType',
label: s__('CiVariables|Type'),
- customStyle: { width: '70px' },
+ thClass: 'gl-w-10p',
},
{
key: 'key',
label: s__('CiVariables|Key'),
tdClass: 'text-plain',
sortable: true,
- customStyle: { width: '40%' },
},
{
key: 'value',
label: s__('CiVariables|Value'),
- customStyle: { width: '40%' },
+ thClass: 'gl-w-15p',
},
{
- key: 'protected',
- label: s__('CiVariables|Protected'),
- customStyle: { width: '100px' },
- },
- {
- key: 'masked',
- label: s__('CiVariables|Masked'),
- customStyle: { width: '100px' },
+ key: 'options',
+ label: s__('CiVariables|Options'),
+ thClass: 'gl-w-10p',
},
{
key: 'environmentScope',
label: s__('CiVariables|Environments'),
- customStyle: { width: '20%' },
},
{
key: 'actions',
label: '',
tdClass: 'text-right',
- customStyle: { width: '35px' },
+ thClass: 'gl-w-5p',
},
],
components: {
- CiVariablePopover,
GlButton,
- GlIcon,
GlLoadingIcon,
GlTable,
- TooltipOnTruncate,
},
directives: {
GlModalDirective,
@@ -97,6 +75,13 @@ export default {
fields() {
return this.$options.fields;
},
+ variablesWithOptions() {
+ return this.variables?.map((item, index) => ({
+ ...item,
+ options: this.getOptions(item),
+ index,
+ }));
+ },
},
methods: {
convertEnvironmentScopeValue(env) {
@@ -108,8 +93,18 @@ export default {
toggleHiddenState() {
this.areValuesHidden = !this.areValuesHidden;
},
- setSelectedVariable(variable = null) {
- this.$emit('set-selected-variable', variable);
+ setSelectedVariable(index = -1) {
+ this.$emit('set-selected-variable', this.variables[index] ?? null);
+ },
+ getOptions(item) {
+ const options = [];
+ if (item.protected) {
+ options.push(s__('CiVariables|Protected'));
+ }
+ if (item.masked) {
+ options.push(s__('CiVariables|Masked'));
+ }
+ return options.join(', ');
},
},
};
@@ -121,7 +116,7 @@ export default {
<gl-table
v-else
:fields="fields"
- :items="variables"
+ :items="variablesWithOptions"
tbody-tr-class="js-ci-variable-row"
data-qa-selector="ci_variable_table_content"
sort-by="key"
@@ -137,23 +132,22 @@ export default {
<col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
</template>
<template #cell(variableType)="{ item }">
- <div class="gl-pt-2">
- {{ generateTypeText(item) }}
- </div>
+ {{ generateTypeText(item) }}
</template>
<template #cell(key)="{ item }">
- <div class="gl-display-flex gl-align-items-center">
- <tooltip-on-truncate :title="item.key" truncate-target="child">
- <span
- :id="`ci-variable-key-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- >{{ item.key }}</span
- >
- </tooltip-on-truncate>
+ <div
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ >
+ <span
+ :id="`ci-variable-key-${item.id}`"
+ class="gl-display-inline-block gl-max-w-full gl-word-break-word"
+ >{{ item.key }}</span
+ >
<gl-button
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
+ class="gl-my-n3 gl-ml-2"
:title="__('Copy key')"
:data-clipboard-text="item.key"
:aria-label="__('Copy to clipboard')"
@@ -161,8 +155,10 @@ export default {
</div>
</template>
<template #cell(value)="{ item }">
- <div class="gl-display-flex gl-align-items-center">
- <span v-if="areValuesHidden" data-testid="hiddenValue">*********************</span>
+ <div
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ >
+ <span v-if="areValuesHidden" data-testid="hiddenValue">*****</span>
<span
v-else
:id="`ci-variable-value-${item.id}`"
@@ -174,31 +170,33 @@ export default {
v-gl-tooltip
category="tertiary"
icon="copy-to-clipboard"
+ class="gl-my-n3 gl-ml-2"
:title="__('Copy value')"
:data-clipboard-text="item.value"
:aria-label="__('Copy to clipboard')"
/>
</div>
</template>
- <template #cell(protected)="{ item }">
- <gl-icon v-if="item.protected" :size="$options.iconSize" :name="$options.trueIcon" />
- <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
- </template>
- <template #cell(masked)="{ item }">
- <gl-icon v-if="item.masked" :size="$options.iconSize" :name="$options.trueIcon" />
- <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
+ <template #cell(options)="{ item }">
+ <span>{{ item.options }}</span>
</template>
<template #cell(environmentScope)="{ item }">
- <div class="gl-display-flex">
+ <div
+ class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
+ >
<span
:id="`ci-variable-env-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ class="gl-display-inline-block gl-max-w-full gl-word-break-word"
>{{ convertEnvironmentScopeValue(item.environmentScope) }}</span
>
- <ci-variable-popover
- :target="`ci-variable-env-${item.id}`"
- :value="convertEnvironmentScopeValue(item.environmentScope)"
- :tooltip-text="__('Copy environment')"
+ <gl-button
+ v-gl-tooltip
+ category="tertiary"
+ icon="copy-to-clipboard"
+ class="gl-my-n3 gl-ml-2"
+ :title="__('Copy environment')"
+ :data-clipboard-text="convertEnvironmentScopeValue(item.environmentScope)"
+ :aria-label="__('Copy to clipboard')"
/>
</div>
</template>
@@ -208,7 +206,7 @@ export default {
icon="pencil"
:aria-label="__('Edit')"
data-qa-selector="edit_ci_variable_button"
- @click="setSelectedVariable(item)"
+ @click="setSelectedVariable(item.index)"
/>
</template>
<template #empty>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
deleted file mode 100644
index ecb39f214ec..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
-import { __, sprintf } from '~/locale';
-
-export default {
- name: 'CiEnvironmentsDropdown',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlSearchBoxByType,
- },
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- searchTerm: '',
- };
- },
- computed: {
- ...mapGetters(['joinedEnvironments']),
- composedCreateButtonLabel() {
- return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
- },
- shouldRenderCreateButton() {
- return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm);
- },
- filteredResults() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.joinedEnvironments.filter((resultString) =>
- resultString.toLowerCase().includes(lowerCasedSearchTerm),
- );
- },
- },
- methods: {
- selectEnvironment(selected) {
- this.$emit('selectEnvironment', selected);
- this.searchTerm = '';
- },
- createClicked() {
- this.$emit('createClicked', this.searchTerm);
- this.searchTerm = '';
- },
- isSelected(env) {
- return this.value === env;
- },
- clearSearch() {
- this.searchTerm = '';
- },
- },
-};
-</script>
-<template>
- <gl-dropdown :text="value" @show="clearSearch">
- <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
- <gl-dropdown-item
- v-for="environment in filteredResults"
- :key="environment"
- :is-checked="isSelected(environment)"
- is-check-item
- @click="selectEnvironment(environment)"
- >
- {{ environment }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
- __('No matching results')
- }}</gl-dropdown-item>
- <template v-if="shouldRenderCreateButton">
- <gl-dropdown-divider />
- <gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked">
- {{ composedCreateButtonLabel }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
deleted file mode 100644
index 1fbe52388c9..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
+++ /dev/null
@@ -1,428 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
-} from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
-import { getCookie, setCookie } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { mapComputed } from '~/vuex_shared/bindings';
-import {
- AWS_TOKEN_CONSTANTS,
- ADD_CI_VARIABLE_MODAL_ID,
- AWS_TIP_DISMISSED_COOKIE_NAME,
- AWS_TIP_MESSAGE,
- CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- EVENT_LABEL,
- EVENT_ACTION,
-} from '../constants';
-import LegacyCiEnvironmentsDropdown from './legacy_ci_environments_dropdown.vue';
-import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
-
-const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
-
-export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- tokens: awsTokens,
- tokenList: awsTokenList,
- awsTipMessage: AWS_TIP_MESSAGE,
- containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
- components: {
- LegacyCiEnvironmentsDropdown,
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
- },
- mixins: [glFeatureFlagsMixin(), trackingMixin],
- data() {
- return {
- isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
- validationErrorEventProperty: '',
- };
- },
- computed: {
- ...mapState([
- 'projectId',
- 'environments',
- 'typeOptions',
- 'variable',
- 'variableBeingEdited',
- 'isGroup',
- 'maskableRegex',
- 'selectedEnvironment',
- 'isProtectedByDefault',
- 'awsLogoSvgPath',
- 'awsTipDeployLink',
- 'awsTipCommandsLink',
- 'awsTipLearnLink',
- 'containsVariableReferenceLink',
- 'protectedEnvironmentVariablesLink',
- 'maskedEnvironmentVariablesLink',
- 'environmentScopeLink',
- ]),
- ...mapComputed(
- [
- { key: 'key', updateFn: 'updateVariableKey' },
- { key: 'secret_value', updateFn: 'updateVariableValue' },
- { key: 'variable_type', updateFn: 'updateVariableType' },
- { key: 'environment_scope', updateFn: 'setEnvironmentScope' },
- { key: 'protected_variable', updateFn: 'updateVariableProtected' },
- { key: 'masked', updateFn: 'updateVariableMasked' },
- ],
- false,
- 'variable',
- ),
- isTipVisible() {
- return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
- },
- canSubmit() {
- return (
- this.variableValidationState &&
- this.variable.key !== '' &&
- this.variable.secret_value !== ''
- );
- },
- canMask() {
- const regex = RegExp(this.maskableRegex);
- return regex.test(this.variable.secret_value);
- },
- containsVariableReference() {
- const regex = /\$/;
- return regex.test(this.variable.secret_value);
- },
- displayMaskedError() {
- return !this.canMask && this.variable.masked;
- },
- maskedState() {
- if (this.displayMaskedError) {
- return false;
- }
- return true;
- },
- modalActionText() {
- return this.variableBeingEdited ? __('Update variable') : __('Add variable');
- },
- maskedFeedback() {
- return this.displayMaskedError ? __('This variable can not be masked.') : '';
- },
- tokenValidationFeedback() {
- const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
- if (!this.tokenValidationState && tokenSpecificFeedback) {
- return tokenSpecificFeedback;
- }
- return '';
- },
- tokenValidationState() {
- const validator = this.$options.tokens?.[this.variable.key]?.validation;
-
- if (validator) {
- return validator(this.variable.secret_value);
- }
-
- return true;
- },
- scopedVariablesAvailable() {
- return !this.isGroup || this.glFeatures.groupScopedCiVariables;
- },
- variableValidationFeedback() {
- return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
- },
- variableValidationState() {
- return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
- },
- },
- watch: {
- variable: {
- handler() {
- this.trackVariableValidationErrors();
- },
- deep: true,
- },
- },
- methods: {
- ...mapActions([
- 'addVariable',
- 'updateVariable',
- 'resetEditing',
- 'displayInputValue',
- 'clearModal',
- 'deleteVariable',
- 'setEnvironmentScope',
- 'addWildCardScope',
- 'resetSelectedEnvironment',
- 'setSelectedEnvironment',
- 'setVariableProtected',
- ]),
- dismissTip() {
- setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
- this.isTipDismissed = true;
- },
- deleteVarAndClose() {
- this.deleteVariable();
- this.hideModal();
- },
- hideModal() {
- this.$refs.modal.hide();
- },
- resetModalHandler() {
- if (this.variableBeingEdited) {
- this.resetEditing();
- }
-
- this.clearModal();
- this.resetSelectedEnvironment();
- this.resetValidationErrorEvents();
- },
- updateOrAddVariable() {
- if (this.variableBeingEdited) {
- this.updateVariable();
- } else {
- this.addVariable();
- }
- this.hideModal();
- },
- setVariableProtectedByDefault() {
- if (this.isProtectedByDefault && !this.variableBeingEdited) {
- this.setVariableProtected();
- }
- },
- trackVariableValidationErrors() {
- const property = this.getTrackingErrorProperty();
- if (!this.validationErrorEventProperty && property) {
- this.track(EVENT_ACTION, { property });
- this.validationErrorEventProperty = property;
- }
- },
- getTrackingErrorProperty() {
- let property;
- if (this.variable.secret_value?.length && !property) {
- if (this.displayMaskedError && this.maskableRegex?.length) {
- const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
- const regex = new RegExp(supportedChars, 'g');
- property = this.variable.secret_value.replace(regex, '');
- }
- if (this.containsVariableReference) {
- property = '$';
- }
- }
-
- return property;
- },
- resetValidationErrorEvents() {
- this.validationErrorEventProperty = '';
- },
- },
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- :modal-id="$options.modalId"
- :title="modalActionText"
- static
- lazy
- @hidden="resetModalHandler"
- @shown="setVariableProtectedByDefault"
- >
- <form>
- <gl-form-combobox
- v-model="key"
- :token-list="$options.tokenList"
- :label-text="__('Key')"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
-
- <gl-form-group
- :label="__('Value')"
- label-for="ci-variable-value"
- :state="variableValidationState"
- :invalid-feedback="variableValidationFeedback"
- >
- <gl-form-textarea
- id="ci-variable-value"
- ref="valueField"
- v-model="secret_value"
- :state="variableValidationState"
- rows="3"
- max-rows="6"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- class="gl-font-monospace!"
- />
- </gl-form-group>
-
- <div class="d-flex">
- <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5">
- <gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
- </gl-form-group>
-
- <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope">
- <template #label>
- {{ __('Environment scope') }}
- <gl-link
- :title="$options.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
- </template>
- <legacy-ci-environments-dropdown
- v-if="scopedVariablesAvailable"
- class="w-100"
- :value="environment_scope"
- @selectEnvironment="setEnvironmentScope"
- @createClicked="addWildCardScope"
- />
-
- <gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
- </gl-form-group>
- </div>
-
- <gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
- <gl-form-checkbox
- v-model="protected_variable"
- class="mb-0"
- data-testid="ci-variable-protected-checkbox"
- >
- {{ __('Protect variable') }}
- <gl-link target="_blank" :href="protectedEnvironmentVariablesLink">
- <gl-icon name="question" :size="12" />
- </gl-link>
- <p class="gl-mt-2 text-secondary">
- {{ __('Export variable to pipelines running on protected branches and tags only.') }}
- </p>
- </gl-form-checkbox>
-
- <gl-form-checkbox
- ref="masked-ci-variable"
- v-model="masked"
- data-testid="ci-variable-masked-checkbox"
- >
- {{ __('Mask variable') }}
- <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
- <gl-icon name="question" :size="12" />
- </gl-link>
- <p class="gl-mt-2 gl-mb-0 text-secondary">
- {{ __('Variable will be masked in job logs.') }}
- <span
- :class="{
- 'bold text-plain': displayMaskedError,
- }"
- >
- {{ __('Requires values to meet regular expression requirements.') }}</span
- >
- <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
- __('More information')
- }}</gl-link>
- </p>
- </gl-form-checkbox>
- </gl-form-group>
- </form>
- <gl-collapse :visible="isTipVisible">
- <gl-alert
- :title="__('Deploying to AWS is easy with GitLab')"
- variant="tip"
- data-testid="aws-guidance-tip"
- @dismiss="dismissTip"
- >
- <div class="gl-display-flex gl-flex-direction-row">
- <div>
- <p>
- <gl-sprintf :message="$options.awsTipMessage">
- <template #deployLink="{ content }">
- <gl-link :href="awsTipDeployLink" target="_blank">{{ content }}</gl-link>
- </template>
- <template #commandsLink="{ content }">
- <gl-link :href="awsTipCommandsLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p>
- <gl-button
- :href="awsTipLearnLink"
- target="_blank"
- category="secondary"
- variant="info"
- class="gl-overflow-wrap-break"
- >{{ __('Learn more about deploying to AWS') }}</gl-button
- >
- </p>
- </div>
- <img
- class="gl-mt-3"
- :alt="__('Amazon Web Services Logo')"
- :src="awsLogoSvgPath"
- height="32"
- />
- </div>
- </gl-alert>
- </gl-collapse>
- <gl-alert
- v-if="containsVariableReference"
- :title="__('Value might contain a variable reference')"
- :dismissible="false"
- variant="warning"
- data-testid="contains-variable-reference"
- >
- <gl-sprintf :message="$options.containsVariableReferenceMessage">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #docsLink="{ content }">
- <gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <template #modal-footer>
- <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
- <gl-button
- v-if="variableBeingEdited"
- ref="deleteCiVariable"
- variant="danger"
- category="secondary"
- data-qa-selector="ci_variable_delete_button"
- @click="deleteVarAndClose"
- >{{ __('Delete variable') }}</gl-button
- >
- <gl-button
- ref="updateOrAddVariable"
- :disabled="!canSubmit"
- variant="confirm"
- category="primary"
- data-testid="ciUpdateOrAddVariableBtn"
- data-qa-selector="ci_variable_save_button"
- @click="updateOrAddVariable"
- >{{ modalActionText }}
- </gl-button>
- </template>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
deleted file mode 100644
index f1fe188348d..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { mapState, mapActions } from 'vuex';
-import LegacyCiVariableModal from './legacy_ci_variable_modal.vue';
-import LegacyCiVariableTable from './legacy_ci_variable_table.vue';
-
-export default {
- components: {
- LegacyCiVariableModal,
- LegacyCiVariableTable,
- },
- computed: {
- ...mapState(['isGroup', 'isProject']),
- },
- mounted() {
- if (this.isProject) {
- this.fetchEnvironments();
- }
- },
- methods: {
- ...mapActions(['fetchEnvironments']),
- },
-};
-</script>
-
-<template>
- <div class="row">
- <div class="col-lg-12">
- <legacy-ci-variable-table />
- <legacy-ci-variable-modal />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
deleted file mode 100644
index f078234829a..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
+++ /dev/null
@@ -1,199 +0,0 @@
-<script>
-import { GlTable, GlButton, GlModalDirective, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { s__, __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
-import CiVariablePopover from './ci_variable_popover.vue';
-
-export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- trueIcon: 'mobile-issue-close',
- falseIcon: 'close',
- iconSize: 16,
- fields: [
- {
- key: 'variable_type',
- label: s__('CiVariables|Type'),
- customStyle: { width: '70px' },
- },
- {
- key: 'key',
- label: s__('CiVariables|Key'),
- tdClass: 'text-plain',
- sortable: true,
- customStyle: { width: '40%' },
- },
- {
- key: 'value',
- label: s__('CiVariables|Value'),
- customStyle: { width: '40%' },
- },
- {
- key: 'protected',
- label: s__('CiVariables|Protected'),
- customStyle: { width: '100px' },
- },
- {
- key: 'masked',
- label: s__('CiVariables|Masked'),
- customStyle: { width: '100px' },
- },
- {
- key: 'environment_scope',
- label: s__('CiVariables|Environments'),
- customStyle: { width: '20%' },
- },
- {
- key: 'actions',
- label: '',
- tdClass: 'text-right',
- customStyle: { width: '35px' },
- },
- ],
- components: {
- CiVariablePopover,
- GlButton,
- GlIcon,
- GlTable,
- TooltipOnTruncate,
- },
- directives: {
- GlModalDirective,
- GlTooltip: GlTooltipDirective,
- },
- mixins: [glFeatureFlagsMixin()],
- computed: {
- ...mapState(['variables', 'valuesHidden', 'isLoading', 'isDeleting']),
- valuesButtonText() {
- return this.valuesHidden ? __('Reveal values') : __('Hide values');
- },
- isTableEmpty() {
- return !this.variables || this.variables.length === 0;
- },
- fields() {
- return this.$options.fields;
- },
- },
- mounted() {
- this.fetchVariables();
- },
- methods: {
- ...mapActions(['fetchVariables', 'toggleValues', 'editVariable']),
- },
-};
-</script>
-
-<template>
- <div class="ci-variable-table" data-testid="ci-variable-table">
- <gl-table
- :fields="fields"
- :items="variables"
- tbody-tr-class="js-ci-variable-row"
- data-qa-selector="ci_variable_table_content"
- sort-by="key"
- sort-direction="asc"
- stacked="lg"
- table-class="text-secondary"
- fixed
- show-empty
- sort-icon-left
- no-sort-reset
- >
- <template #table-colgroup="scope">
- <col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
- </template>
- <template #cell(key)="{ item }">
- <div class="gl-display-flex gl-align-items-center">
- <tooltip-on-truncate :title="item.key" truncate-target="child">
- <span
- :id="`ci-variable-key-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- >{{ item.key }}</span
- >
- </tooltip-on-truncate>
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- :title="__('Copy key')"
- :data-clipboard-text="item.key"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </template>
- <template #cell(value)="{ item }">
- <div class="gl-display-flex gl-align-items-center">
- <span v-if="valuesHidden">*********************</span>
- <span
- v-else
- :id="`ci-variable-value-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- >{{ item.value }}</span
- >
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- :title="__('Copy value')"
- :data-clipboard-text="item.value"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </template>
- <template #cell(protected)="{ item }">
- <gl-icon v-if="item.protected" :size="$options.iconSize" :name="$options.trueIcon" />
- <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
- </template>
- <template #cell(masked)="{ item }">
- <gl-icon v-if="item.masked" :size="$options.iconSize" :name="$options.trueIcon" />
- <gl-icon v-else :size="$options.iconSize" :name="$options.falseIcon" />
- </template>
- <template #cell(environment_scope)="{ item }">
- <div class="gl-display-flex">
- <span
- :id="`ci-variable-env-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- >{{ item.environment_scope }}</span
- >
- <ci-variable-popover
- :target="`ci-variable-env-${item.id}`"
- :value="item.environment_scope"
- :tooltip-text="__('Copy environment')"
- />
- </div>
- </template>
- <template #cell(actions)="{ item }">
- <gl-button
- v-gl-modal-directive="$options.modalId"
- icon="pencil"
- :aria-label="__('Edit')"
- data-qa-selector="edit_ci_variable_button"
- @click="editVariable(item)"
- />
- </template>
- <template #empty>
- <p class="gl-text-center gl-py-6 gl-text-black-normal gl-mb-0">
- {{ __('There are no variables yet.') }}
- </p>
- </template>
- </gl-table>
- <div class="ci-variable-actions gl-display-flex gl-mt-5">
- <gl-button
- v-gl-modal-directive="$options.modalId"
- class="gl-mr-3"
- data-qa-selector="add_ci_variable_button"
- variant="confirm"
- category="primary"
- >{{ __('Add variable') }}</gl-button
- >
- <gl-button
- v-if="!isTableEmpty"
- data-qa-selector="reveal_ci_variable_value_button"
- @click="toggleValues(!valuesHidden)"
- >{{ valuesButtonText }}</gl-button
- >
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
index eba4b0c32f8..9208c34f154 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
@@ -1,7 +1,7 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addAdminVariable($variable: CiVariable!, $endpoint: String!) {
- addAdminVariable(variable: $variable, endpoint: $endpoint) @client {
+ ciVariableMutation: addAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariables {
nodes {
...BaseCiVariable
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
index 96eb8c794bc..a79b98f5e95 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
@@ -1,7 +1,7 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteAdminVariable($variable: CiVariable!, $endpoint: String!) {
- deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client {
+ ciVariableMutation: deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariables {
nodes {
...BaseCiVariable
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
index c0388507bb8..ddea753bf90 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
@@ -1,7 +1,7 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateAdminVariable($variable: CiVariable!, $endpoint: String!) {
- updateAdminVariable(variable: $variable, endpoint: $endpoint) @client {
+ ciVariableMutation: updateAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariables {
nodes {
...BaseCiVariable
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
index f8e4dc55fa4..c44ee2ecc1d 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
@@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
-mutation addGroupVariable(
- $variable: CiVariable!
- $endpoint: String!
- $fullPath: ID!
- $groupId: ID!
-) {
- addGroupVariable(
+mutation addGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
+ ciVariableMutation: addGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
- groupId: $groupId
+ id: $id
) @client {
group {
id
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
index 310e4a6e551..53e9b411dd2 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
@@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
-mutation deleteGroupVariable(
- $variable: CiVariable!
- $endpoint: String!
- $fullPath: ID!
- $groupId: ID!
-) {
- deleteGroupVariable(
+mutation deleteGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
+ ciVariableMutation: deleteGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
- groupId: $groupId
+ id: $id
) @client {
group {
id
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
index 5291942eb87..2dddca14bd8 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
@@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
-mutation updateGroupVariable(
- $variable: CiVariable!
- $endpoint: String!
- $fullPath: ID!
- $groupId: ID!
-) {
- updateGroupVariable(
+mutation updateGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
+ ciVariableMutation: updateGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
- groupId: $groupId
+ id: $id
) @client {
group {
id
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
index ab3a46da854..39504770e33 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
@@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
-mutation addProjectVariable(
- $variable: CiVariable!
- $endpoint: String!
- $fullPath: ID!
- $projectId: ID!
-) {
- addProjectVariable(
+mutation addProjectVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
+ ciVariableMutation: addProjectVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
- projectId: $projectId
+ id: $id
) @client {
project {
id
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
index e83dc9a5e5e..f55c255e332 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
@@ -4,13 +4,13 @@ mutation deleteProjectVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
- $projectId: ID!
+ $id: ID!
) {
- deleteProjectVariable(
+ ciVariableMutation: deleteProjectVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
- projectId: $projectId
+ id: $id
) @client {
project {
id
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
index 4788911431b..fc589e8a939 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
@@ -4,13 +4,13 @@ mutation updateProjectVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
- $projectId: ID!
+ $id: ID!
) {
- updateProjectVariable(
+ ciVariableMutation: updateProjectVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
- projectId: $projectId
+ id: $id
) @client {
project {
id
diff --git a/app/assets/javascripts/ci_variable_list/graphql/settings.js b/app/assets/javascripts/ci_variable_list/graphql/settings.js
index ecdc4f220bd..02f6c226b0f 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/settings.js
+++ b/app/assets/javascripts/ci_variable_list/graphql/settings.js
@@ -36,12 +36,12 @@ const mapVariableTypes = (variables = [], kind) => {
});
};
-const prepareProjectGraphQLResponse = ({ data, projectId, errors = [] }) => {
+const prepareProjectGraphQLResponse = ({ data, id, errors = [] }) => {
return {
errors,
project: {
__typename: GRAPHQL_PROJECT_TYPE,
- id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, projectId),
+ id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, id),
ciVariables: {
__typename: `Ci${GRAPHQL_PROJECT_TYPE}VariableConnection`,
pageInfo: {
@@ -57,12 +57,12 @@ const prepareProjectGraphQLResponse = ({ data, projectId, errors = [] }) => {
};
};
-const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => {
+const prepareGroupGraphQLResponse = ({ data, id, errors = [] }) => {
return {
errors,
group: {
__typename: GRAPHQL_GROUP_TYPE,
- id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, groupId),
+ id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, id),
ciVariables: {
__typename: `Ci${GRAPHQL_GROUP_TYPE}VariableConnection`,
pageInfo: {
@@ -95,20 +95,13 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
};
};
-async function callProjectEndpoint({
- endpoint,
- fullPath,
- variable,
- projectId,
- cache,
- destroy = false,
-}) {
+async function callProjectEndpoint({ endpoint, fullPath, variable, id, cache, destroy = false }) {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- const graphqlData = prepareProjectGraphQLResponse({ data, projectId });
+ const graphqlData = prepareProjectGraphQLResponse({ data, id });
cache.writeQuery({
query: getProjectVariables,
@@ -122,26 +115,19 @@ async function callProjectEndpoint({
} catch (e) {
return prepareProjectGraphQLResponse({
data: cache.readQuery({ query: getProjectVariables, variables: { fullPath } }),
- projectId,
+ id,
errors: [...e.response.data],
});
}
}
-const callGroupEndpoint = async ({
- endpoint,
- fullPath,
- variable,
- groupId,
- cache,
- destroy = false,
-}) => {
+const callGroupEndpoint = async ({ endpoint, fullPath, variable, id, cache, destroy = false }) => {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- const graphqlData = prepareGroupGraphQLResponse({ data, groupId });
+ const graphqlData = prepareGroupGraphQLResponse({ data, id });
cache.writeQuery({
query: getGroupVariables,
@@ -152,7 +138,7 @@ const callGroupEndpoint = async ({
} catch (e) {
return prepareGroupGraphQLResponse({
data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }),
- groupId,
+ id,
errors: [...e.response.data],
});
}
@@ -182,23 +168,23 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false })
export const resolvers = {
Mutation: {
- addProjectVariable: async (_, { endpoint, fullPath, variable, projectId }, { cache }) => {
- return callProjectEndpoint({ endpoint, fullPath, variable, projectId, cache });
+ addProjectVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
+ return callProjectEndpoint({ endpoint, fullPath, variable, id, cache });
},
- updateProjectVariable: async (_, { endpoint, fullPath, variable, projectId }, { cache }) => {
- return callProjectEndpoint({ endpoint, fullPath, variable, projectId, cache });
+ updateProjectVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
+ return callProjectEndpoint({ endpoint, fullPath, variable, id, cache });
},
- deleteProjectVariable: async (_, { endpoint, fullPath, variable, projectId }, { cache }) => {
- return callProjectEndpoint({ endpoint, fullPath, variable, projectId, cache, destroy: true });
+ deleteProjectVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
+ return callProjectEndpoint({ endpoint, fullPath, variable, id, cache, destroy: true });
},
- addGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
- return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
+ addGroupVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
+ return callGroupEndpoint({ endpoint, fullPath, variable, id, cache });
},
- updateGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
- return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
+ updateGroupVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
+ return callGroupEndpoint({ endpoint, fullPath, variable, id, cache });
},
- deleteGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
- return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache, destroy: true });
+ deleteGroupVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
+ return callGroupEndpoint({ endpoint, fullPath, variable, id, cache, destroy: true });
},
addAdminVariable: async (_, { endpoint, variable }, { cache }) => {
return callAdminEndpoint({ endpoint, variable, cache });
@@ -238,7 +224,7 @@ export const cacheConfig = {
Project: {
fields: {
ciVariables: {
- keyArgs: ['fullPath', 'endpoint', 'projectId'],
+ keyArgs: ['fullPath', 'endpoint', 'id'],
merge: mergeVariables,
},
},
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 1b69da9e086..174a59aba42 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -5,9 +5,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import CiAdminVariables from './components/ci_admin_variables.vue';
import CiGroupVariables from './components/ci_group_variables.vue';
import CiProjectVariables from './components/ci_project_variables.vue';
-import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import { cacheConfig, resolvers } from './graphql/settings';
-import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
const {
@@ -76,62 +74,10 @@ const mountCiVariableListApp = (containerEl) => {
});
};
-const mountLegacyCiVariableListApp = (containerEl) => {
- const {
- endpoint,
- projectId,
- isGroup,
- isProject,
- maskableRegex,
- protectedByDefault,
- awsLogoSvgPath,
- awsTipDeployLink,
- awsTipCommandsLink,
- awsTipLearnLink,
- containsVariableReferenceLink,
- protectedEnvironmentVariablesLink,
- maskedEnvironmentVariablesLink,
- environmentScopeLink,
- } = containerEl.dataset;
-
- const parsedIsProject = parseBoolean(isProject);
- const parsedIsGroup = parseBoolean(isGroup);
- const isProtectedByDefault = parseBoolean(protectedByDefault);
-
- const store = createStore({
- endpoint,
- projectId,
- isGroup: parsedIsGroup,
- isProject: parsedIsProject,
- maskableRegex,
- isProtectedByDefault,
- awsLogoSvgPath,
- awsTipDeployLink,
- awsTipCommandsLink,
- awsTipLearnLink,
- containsVariableReferenceLink,
- protectedEnvironmentVariablesLink,
- maskedEnvironmentVariablesLink,
- environmentScopeLink,
- });
-
- return new Vue({
- el: containerEl,
- store,
- render(createElement) {
- return createElement(LegacyCiVariableSettings);
- },
- });
-};
-
-export default (containerId = 'js-ci-project-variables') => {
+export default (containerId = 'js-ci-variables') => {
const el = document.getElementById(containerId);
- if (el) {
- if (gon.features?.ciVariableSettingsGraphql) {
- mountCiVariableListApp(el);
- } else {
- mountLegacyCiVariableListApp(el);
- }
- }
+ if (!el) return;
+
+ mountCiVariableListApp(el);
};
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
deleted file mode 100644
index ac31e845b0d..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import Api from '~/api';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import * as types from './mutation_types';
-import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils';
-
-export const toggleValues = ({ commit }, valueState) => {
- commit(types.TOGGLE_VALUES, valueState);
-};
-
-export const clearModal = ({ commit }) => {
- commit(types.CLEAR_MODAL);
-};
-
-export const resetEditing = ({ commit, dispatch }) => {
- // fetch variables again if modal is being edited and then hidden
- // without saving changes, to cover use case of reactivity in the table
- dispatch('fetchVariables');
- commit(types.RESET_EDITING);
-};
-
-export const setVariableProtected = ({ commit }) => {
- commit(types.SET_VARIABLE_PROTECTED);
-};
-
-export const requestAddVariable = ({ commit }) => {
- commit(types.REQUEST_ADD_VARIABLE);
-};
-
-export const receiveAddVariableSuccess = ({ commit }) => {
- commit(types.RECEIVE_ADD_VARIABLE_SUCCESS);
-};
-
-export const receiveAddVariableError = ({ commit }, error) => {
- commit(types.RECEIVE_ADD_VARIABLE_ERROR, error);
-};
-
-export const addVariable = ({ state, dispatch }) => {
- dispatch('requestAddVariable');
-
- return axios
- .patch(state.endpoint, {
- variables_attributes: [prepareDataForApi(state.variable)],
- })
- .then(() => {
- dispatch('receiveAddVariableSuccess');
- dispatch('fetchVariables');
- })
- .catch((error) => {
- createAlert({
- message: error.response.data[0],
- });
- dispatch('receiveAddVariableError', error);
- });
-};
-
-export const requestUpdateVariable = ({ commit }) => {
- commit(types.REQUEST_UPDATE_VARIABLE);
-};
-
-export const receiveUpdateVariableSuccess = ({ commit }) => {
- commit(types.RECEIVE_UPDATE_VARIABLE_SUCCESS);
-};
-
-export const receiveUpdateVariableError = ({ commit }, error) => {
- commit(types.RECEIVE_UPDATE_VARIABLE_ERROR, error);
-};
-
-export const updateVariable = ({ state, dispatch }) => {
- dispatch('requestUpdateVariable');
-
- const updatedVariable = prepareDataForApi(state.variable);
- updatedVariable.secrect_value = updateVariable.value;
-
- return axios
- .patch(state.endpoint, { variables_attributes: [updatedVariable] })
- .then(() => {
- dispatch('receiveUpdateVariableSuccess');
- dispatch('fetchVariables');
- })
- .catch((error) => {
- createAlert({
- message: error.response.data[0],
- });
- dispatch('receiveUpdateVariableError', error);
- });
-};
-
-export const editVariable = ({ commit }, variable) => {
- const variableToEdit = variable;
- variableToEdit.secret_value = variableToEdit.value;
- commit(types.VARIABLE_BEING_EDITED, variableToEdit);
-};
-
-export const requestVariables = ({ commit }) => {
- commit(types.REQUEST_VARIABLES);
-};
-export const receiveVariablesSuccess = ({ commit }, variables) => {
- commit(types.RECEIVE_VARIABLES_SUCCESS, variables);
-};
-
-export const fetchVariables = ({ dispatch, state }) => {
- dispatch('requestVariables');
-
- return axios
- .get(state.endpoint)
- .then(({ data }) => {
- dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables));
- })
- .catch(() => {
- createAlert({
- message: __('There was an error fetching the variables.'),
- });
- });
-};
-
-export const requestDeleteVariable = ({ commit }) => {
- commit(types.REQUEST_DELETE_VARIABLE);
-};
-
-export const receiveDeleteVariableSuccess = ({ commit }) => {
- commit(types.RECEIVE_DELETE_VARIABLE_SUCCESS);
-};
-
-export const receiveDeleteVariableError = ({ commit }, error) => {
- commit(types.RECEIVE_DELETE_VARIABLE_ERROR, error);
-};
-
-export const deleteVariable = ({ dispatch, state }) => {
- dispatch('requestDeleteVariable');
-
- const destroy = true;
-
- return axios
- .patch(state.endpoint, { variables_attributes: [prepareDataForApi(state.variable, destroy)] })
- .then(() => {
- dispatch('receiveDeleteVariableSuccess');
- dispatch('fetchVariables');
- })
- .catch((error) => {
- createAlert({
- message: error.response.data[0],
- });
- dispatch('receiveDeleteVariableError', error);
- });
-};
-
-export const requestEnvironments = ({ commit }) => {
- commit(types.REQUEST_ENVIRONMENTS);
-};
-
-export const receiveEnvironmentsSuccess = ({ commit }, environments) => {
- commit(types.RECEIVE_ENVIRONMENTS_SUCCESS, environments);
-};
-
-export const fetchEnvironments = ({ dispatch, state }) => {
- dispatch('requestEnvironments');
-
- return Api.environments(state.projectId)
- .then((res) => {
- dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data));
- })
- .catch(() => {
- createAlert({
- message: __('There was an error fetching the environments information.'),
- });
- });
-};
-
-export const setEnvironmentScope = ({ commit, dispatch }, environment) => {
- commit(types.SET_ENVIRONMENT_SCOPE, environment);
- dispatch('setSelectedEnvironment', environment);
-};
-
-export const addWildCardScope = ({ commit, dispatch }, environment) => {
- commit(types.ADD_WILD_CARD_SCOPE, environment);
- commit(types.SET_ENVIRONMENT_SCOPE, environment);
- dispatch('setSelectedEnvironment', environment);
-};
-
-export const resetSelectedEnvironment = ({ commit }) => {
- commit(types.RESET_SELECTED_ENVIRONMENT);
-};
-
-export const setSelectedEnvironment = ({ commit }, environment) => {
- commit(types.SET_SELECTED_ENVIRONMENT, environment);
-};
-
-export const updateVariableKey = ({ commit }, { key }) => {
- commit(types.UPDATE_VARIABLE_KEY, key);
-};
-
-export const updateVariableValue = ({ commit }, { secret_value }) => {
- commit(types.UPDATE_VARIABLE_VALUE, secret_value);
-};
-
-export const updateVariableType = ({ commit }, { variable_type }) => {
- commit(types.UPDATE_VARIABLE_TYPE, variable_type);
-};
-
-export const updateVariableProtected = ({ commit }, { protected_variable }) => {
- commit(types.UPDATE_VARIABLE_PROTECTED, protected_variable);
-};
-
-export const updateVariableMasked = ({ commit }, { masked }) => {
- commit(types.UPDATE_VARIABLE_MASKED, masked);
-};
diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js
deleted file mode 100644
index 6570f455541..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/getters.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { uniq } from 'lodash';
-
-export const joinedEnvironments = (state) => {
- const scopesFromVariables = (state.variables || []).map((variable) => variable.environment_scope);
- return uniq(state.environments.concat(scopesFromVariables)).sort();
-};
diff --git a/app/assets/javascripts/ci_variable_list/store/index.js b/app/assets/javascripts/ci_variable_list/store/index.js
deleted file mode 100644
index 83802f6a36f..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export default (initialState = {}) =>
- new Vuex.Store({
- actions,
- mutations,
- getters,
- state: {
- ...state(),
- ...initialState,
- },
- });
diff --git a/app/assets/javascripts/ci_variable_list/store/mutation_types.js b/app/assets/javascripts/ci_variable_list/store/mutation_types.js
deleted file mode 100644
index 5db8f610192..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/mutation_types.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export const TOGGLE_VALUES = 'TOGGLE_VALUES';
-export const VARIABLE_BEING_EDITED = 'VARIABLE_BEING_EDITED';
-export const RESET_EDITING = 'RESET_EDITING';
-export const CLEAR_MODAL = 'CLEAR_MODAL';
-export const SET_VARIABLE_PROTECTED = 'SET_VARIABLE_PROTECTED';
-
-export const REQUEST_VARIABLES = 'REQUEST_VARIABLES';
-export const RECEIVE_VARIABLES_SUCCESS = 'RECEIVE_VARIABLES_SUCCESS';
-
-export const REQUEST_DELETE_VARIABLE = 'REQUEST_DELETE_VARIABLE';
-export const RECEIVE_DELETE_VARIABLE_SUCCESS = 'RECEIVE_DELETE_VARIABLE_SUCCESS';
-export const RECEIVE_DELETE_VARIABLE_ERROR = 'RECEIVE_DELETE_VARIABLE_ERROR';
-
-export const REQUEST_ADD_VARIABLE = 'REQUEST_ADD_VARIABLE';
-export const RECEIVE_ADD_VARIABLE_SUCCESS = 'RECEIVE_ADD_VARIABLE_SUCCESS';
-export const RECEIVE_ADD_VARIABLE_ERROR = 'RECEIVE_ADD_VARIABLE_ERROR';
-
-export const REQUEST_UPDATE_VARIABLE = 'REQUEST_UPDATE_VARIABLE';
-export const RECEIVE_UPDATE_VARIABLE_SUCCESS = 'RECEIVE_UPDATE_VARIABLE_SUCCESS';
-export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR';
-
-export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
-export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
-export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
-export const ADD_WILD_CARD_SCOPE = 'ADD_WILD_CARD_SCOPE';
-export const RESET_SELECTED_ENVIRONMENT = 'RESET_SELECTED_ENVIRONMENT';
-export const SET_SELECTED_ENVIRONMENT = 'SET_SELECTED_ENVIRONMENT';
-
-export const UPDATE_VARIABLE_KEY = 'UPDATE_VARIABLE_KEY';
-export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE';
-export const UPDATE_VARIABLE_TYPE = 'UPDATE_VARIABLE_TYPE';
-export const UPDATE_VARIABLE_PROTECTED = 'UPDATE_VARIABLE_PROTECTED';
-export const UPDATE_VARIABLE_MASKED = 'UPDATE_VARIABLE_MASKED';
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
deleted file mode 100644
index 0e7c61cecb8..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { displayText } from '../constants';
-import * as types from './mutation_types';
-
-export default {
- [types.REQUEST_VARIABLES](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_VARIABLES_SUCCESS](state, variables) {
- state.isLoading = false;
- state.variables = variables;
- },
-
- [types.REQUEST_DELETE_VARIABLE](state) {
- state.isDeleting = true;
- },
-
- [types.RECEIVE_DELETE_VARIABLE_SUCCESS](state) {
- state.isDeleting = false;
- },
-
- [types.RECEIVE_DELETE_VARIABLE_ERROR](state, error) {
- state.isDeleting = false;
- state.error = error;
- },
-
- [types.REQUEST_ADD_VARIABLE](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_ADD_VARIABLE_SUCCESS](state) {
- state.isLoading = false;
- },
-
- [types.RECEIVE_ADD_VARIABLE_ERROR](state, error) {
- state.isLoading = false;
- state.error = error;
- },
-
- [types.REQUEST_UPDATE_VARIABLE](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_UPDATE_VARIABLE_SUCCESS](state) {
- state.isLoading = false;
- },
-
- [types.RECEIVE_UPDATE_VARIABLE_ERROR](state, error) {
- state.isLoading = false;
- state.error = error;
- },
-
- [types.TOGGLE_VALUES](state, valueState) {
- state.valuesHidden = valueState;
- },
-
- [types.REQUEST_ENVIRONMENTS](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) {
- state.isLoading = false;
- state.environments = environments;
- state.environments.unshift(displayText.allEnvironmentsText);
- },
-
- [types.VARIABLE_BEING_EDITED](state, variable) {
- state.variableBeingEdited = true;
- state.variable = variable;
- },
-
- [types.CLEAR_MODAL](state) {
- state.variable = {
- variable_type: displayText.variableText,
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: displayText.allEnvironmentsText,
- };
- },
-
- [types.RESET_EDITING](state) {
- state.variableBeingEdited = false;
- state.showInputValue = false;
- },
-
- [types.SET_ENVIRONMENT_SCOPE](state, environment) {
- state.variable.environment_scope = environment;
- },
-
- [types.ADD_WILD_CARD_SCOPE](state, environment) {
- state.environments.push(environment);
- state.environments.sort();
- },
-
- [types.RESET_SELECTED_ENVIRONMENT](state) {
- state.selectedEnvironment = '';
- },
-
- [types.SET_SELECTED_ENVIRONMENT](state, environment) {
- state.selectedEnvironment = environment;
- },
-
- [types.SET_VARIABLE_PROTECTED](state) {
- state.variable.protected_variable = true;
- },
-
- [types.UPDATE_VARIABLE_KEY](state, key) {
- state.variable.key = key;
- },
-
- [types.UPDATE_VARIABLE_VALUE](state, value) {
- state.variable.secret_value = value;
- },
-
- [types.UPDATE_VARIABLE_TYPE](state, type) {
- state.variable.variable_type = type;
- },
-
- [types.UPDATE_VARIABLE_PROTECTED](state, bool) {
- state.variable.protected_variable = bool;
- },
-
- [types.UPDATE_VARIABLE_MASKED](state, bool) {
- state.variable.masked = bool;
- },
-};
diff --git a/app/assets/javascripts/ci_variable_list/store/state.js b/app/assets/javascripts/ci_variable_list/store/state.js
deleted file mode 100644
index 96b27792664..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/state.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { displayText } from '../constants';
-
-export default () => ({
- endpoint: null,
- projectId: null,
- isGroup: null,
- maskableRegex: null,
- isProtectedByDefault: null,
- isLoading: false,
- isDeleting: false,
- variable: {
- variable_type: displayText.variableText,
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: displayText.allEnvironmentsText,
- },
- variables: null,
- valuesHidden: true,
- error: null,
- environments: [],
- typeOptions: [displayText.variableText, displayText.fileText],
- variableBeingEdited: false,
- selectedEnvironment: '',
-});
diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js
deleted file mode 100644
index f46a671ae7b..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/utils.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { cloneDeep } from 'lodash';
-import { displayText, types, allEnvironments } from '../constants';
-
-const variableTypeHandler = (type) =>
- type === displayText.variableText ? types.variableType : types.fileType;
-
-export const prepareDataForDisplay = (variables) => {
- const variablesToDisplay = [];
- variables.forEach((variable) => {
- const variableCopy = variable;
- if (variableCopy.variable_type === types.variableType) {
- variableCopy.variable_type = displayText.variableText;
- } else {
- variableCopy.variable_type = displayText.fileText;
- }
- variableCopy.secret_value = variableCopy.value;
-
- if (variableCopy.environment_scope === allEnvironments.type) {
- variableCopy.environment_scope = displayText.allEnvironmentsText;
- }
- variableCopy.protected_variable = variableCopy.protected;
- variablesToDisplay.push(variableCopy);
- });
- return variablesToDisplay;
-};
-
-export const prepareDataForApi = (variable, destroy = false) => {
- const variableCopy = cloneDeep(variable);
- variableCopy.protected = variableCopy.protected_variable.toString();
- delete variableCopy.protected_variable;
- variableCopy.masked = variableCopy.masked.toString();
- variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type);
- if (variableCopy.environment_scope === displayText.allEnvironmentsText) {
- variableCopy.environment_scope = allEnvironments.type;
- }
-
- if (destroy) {
- // eslint-disable-next-line
- variableCopy._destroy = destroy;
- }
-
- return variableCopy;
-};
-
-export const prepareEnvironments = (environments) => environments.map((e) => e.name);
diff --git a/app/assets/javascripts/clusters_list/components/delete_agent_button.vue b/app/assets/javascripts/clusters_list/components/delete_agent_button.vue
index 6f2c353a67b..7a028858d10 100644
--- a/app/assets/javascripts/clusters_list/components/delete_agent_button.vue
+++ b/app/assets/javascripts/clusters_list/components/delete_agent_button.vue
@@ -92,6 +92,9 @@ export default {
disableModalSubmit() {
return this.deleteConfirmText !== this.agent.name;
},
+ containerTabIndex() {
+ return this.canAdminCluster ? -1 : 0;
+ },
},
methods: {
async deleteAgent() {
@@ -156,8 +159,8 @@ export default {
<div>
<div
v-gl-tooltip="deleteButtonTooltip"
- class="gl-display-inline-block"
- tabindex="-1"
+ :tabindex="containerTabIndex"
+ class="cluster-button-container gl-rounded-base gl-display-inline-block"
data-testid="delete-agent-button-tooltip"
>
<gl-button
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
index 327b0967229..354db88f11c 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/formatting_bubble_menu.vue
@@ -108,6 +108,16 @@ export default {
@execute="trackToolbarControlExecution"
/>
<toolbar-button
+ data-testid="highlight"
+ content-type="highlight"
+ icon-name="highlight"
+ editor-command="toggleHighlight"
+ category="tertiary"
+ size="medium"
+ :label="__('Highlight')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
data-testid="link"
content-type="link"
icon-name="link"
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index 987b7044272..001b34a00fa 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -90,6 +90,9 @@ export default {
items() {
this.selectedIndex = 0;
},
+ selectedIndex() {
+ this.scrollIntoView();
+ },
},
methods: {
@@ -182,6 +185,10 @@ export default {
this.selectItem(this.selectedIndex);
},
+ scrollIntoView() {
+ this.$refs.dropdownItems[this.selectedIndex].$el.scrollIntoView({ block: 'nearest' });
+ },
+
selectItem(index) {
const item = this.items[index];
@@ -209,6 +216,7 @@ export default {
<div class="gl-new-dropdown-inner gl-overflow-y-auto">
<gl-dropdown-item
v-for="(item, index) in items"
+ ref="dropdownItems"
:key="index"
:class="{ 'gl-bg-gray-50': index === selectedIndex }"
@click="selectItem(index)"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
index c16dc34e36f..cef026c5bc6 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -32,7 +32,7 @@ export default {
editorCommandParams: {
type: Object,
required: false,
- default: null,
+ default: undefined,
},
variant: {
type: String,
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
index 2d4226ccd33..9e1a4bfe361 100644
--- a/app/assets/javascripts/content_editor/content_editor.stories.js
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -11,7 +11,6 @@ const Template = (_, { argTypes }) => ({
template: '<content-editor v-bind="$props" @initialized="loadContent" />',
methods: {
loadContent(contentEditor) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
contentEditor.setSerializedContent('Hello content editor');
},
},
diff --git a/app/assets/javascripts/content_editor/extensions/highlight.js b/app/assets/javascripts/content_editor/extensions/highlight.js
new file mode 100644
index 00000000000..b84388d6285
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/highlight.js
@@ -0,0 +1,19 @@
+import { markInputRule } from '@tiptap/core';
+import { Highlight } from '@tiptap/extension-highlight';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+export default Highlight.extend({
+ addInputRules() {
+ return [
+ markInputRule({
+ find: markInputRegex('mark'),
+ type: this.type,
+ getAttributes: extractMarkAttributesFromMatch,
+ }),
+ ];
+ },
+
+ addPasteRules() {
+ return [];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
index 9579f3b06f6..79fc0eea2c7 100644
--- a/app/assets/javascripts/content_editor/extensions/html_marks.js
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -8,7 +8,6 @@ const marks = [
'bdo',
'cite',
'dfn',
- 'mark',
'small',
'span',
'time',
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index 8976b9cafee..a9628c78add 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -17,7 +17,7 @@ function createSuggestionPlugin({
char,
dataSource,
search,
- limit = Infinity,
+ limit = 15,
nodeType,
nodeProps = {},
}) {
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 0d78390e769..ba9ce705c62 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -29,6 +29,7 @@ import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import History from '../extensions/history';
+import Highlight from '../extensions/highlight';
import HorizontalRule from '../extensions/horizontal_rule';
import HTMLMarks from '../extensions/html_marks';
import HTMLNodes from '../extensions/html_nodes';
@@ -118,6 +119,7 @@ export const createContentEditor = ({
HardBreak,
Heading,
History,
+ Highlight,
HorizontalRule,
...HTMLMarks,
...HTMLNodes,
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index c990f6cf0b3..958c27c281a 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -22,6 +22,7 @@ import Frontmatter from '../extensions/frontmatter';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule';
+import Highlight from '../extensions/highlight';
import HTMLMarks from '../extensions/html_marks';
import HTMLNodes from '../extensions/html_nodes';
import Image from '../extensions/image';
@@ -78,6 +79,7 @@ const defaultSerializerConfig = {
[Code.name]: code,
[Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
[Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
+ [Highlight.name]: { open: '<mark>', close: '</mark>', mixable: true },
[InlineDiff.name]: {
mixable: true,
open(_, mark) {
diff --git a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
index 016fea354fe..0ad325a8523 100644
--- a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
+++ b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
@@ -1,9 +1,12 @@
<script>
import { mapActions, mapState } from 'vuex';
-import { __ } from '~/locale';
import {
OPERATOR_IS_ONLY,
DEFAULT_NONE_ANY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
@@ -43,7 +46,7 @@ export default {
return [
{
icon: 'clock',
- title: __('Milestone'),
+ title: TOKEN_TITLE_MILESTONE,
type: 'milestone',
token: MilestoneToken,
initialMilestones: this.milestonesData,
@@ -54,7 +57,7 @@ export default {
},
{
icon: 'labels',
- title: __('Label'),
+ title: TOKEN_TITLE_LABEL,
type: 'labels',
token: LabelToken,
defaultLabels: DEFAULT_NONE_ANY,
@@ -66,7 +69,7 @@ export default {
},
{
icon: 'pencil',
- title: __('Author'),
+ title: TOKEN_TITLE_AUTHOR,
type: 'author',
token: AuthorToken,
initialAuthors: this.authorsData,
@@ -76,7 +79,7 @@ export default {
},
{
icon: 'user',
- title: __('Assignees'),
+ title: TOKEN_TITLE_ASSIGNEE,
type: 'assignees',
token: AuthorToken,
initialAuthors: this.assigneesData,
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 639dd21bd7b..81d74c64124 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -109,10 +109,11 @@ export default {
writePackageRegistryHelp: s__(
'DeployTokens|Allows read and write access to the package registry.',
),
+ createTokenFailedAlert: s__('DeployTokens|Failed to create a new deployment token'),
},
computed: {
formattedExpiryDate() {
- return formatDate(this.expiresAt, 'yyyy-mm-dd');
+ return this.expiresAt ? formatDate(this.expiresAt, 'yyyy-mm-dd') : '';
},
newTokenCreatedMessage() {
return this.tokenType === 'group'
@@ -129,6 +130,9 @@ export default {
name: this.name,
read_repository: this.readRepository,
read_registry: this.readRegistry,
+ write_registry: this.writeRegistry,
+ read_package_registry: this.readPackageRegistry,
+ write_package_registry: this.writePackageRegistry,
username: this.username,
},
})
@@ -142,7 +146,8 @@ export default {
})
.catch((error) => {
createAlert({
- message: error.response.data.message,
+ message:
+ error?.response?.data?.message || this.$options.translations.createTokenFailedAlert,
});
});
},
@@ -228,13 +233,7 @@ 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"
- class="qa-deploy-token-name"
- data-qa-selector="deploy_token_name_field"
- />
+ <gl-form-input id="deploy_token_name" v-model="name" name="deploy_token_name" />
</gl-form-group>
<gl-form-group
:label="$options.translations.addTokenExpiryLabel"
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index fba73cd4bec..ab003fb2879 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -349,7 +349,7 @@ export default {
<template>
<div
data-testid="designs-root"
- class="gl-mt-5"
+ class="gl-mt-4"
@mouseenter="toggleOnPasteListener"
@mouseleave="toggleOffPasteListener"
>
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index bc49464a560..7bc75127876 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -471,8 +471,14 @@ export default {
},
fetchData(toggleTree = true) {
this.fetchDiffFilesMeta()
- .then(({ real_size = 0 }) => {
- this.diffFilesLength = parseInt(real_size, 10) || 0;
+ .then((data) => {
+ let realSize = 0;
+
+ if (data) {
+ realSize = data.real_size;
+ }
+
+ this.diffFilesLength = parseInt(realSize, 10) || 0;
if (toggleTree) {
this.setTreeDisplay();
}
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 25d3bda147b..9e399a642d0 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -88,6 +88,7 @@ export default {
:discussions-by-diff-order="true"
:line="line"
:help-page-path="helpPagePath"
+ :should-scroll-to-note="false"
@noteDeleted="deleteNoteHandler"
>
<template v-if="renderAvatarBadge" #avatar-badge>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 422bf52a1fa..8f041d1e670 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -393,7 +393,7 @@ export default {
v-else-if="conflictResolutionPath"
:message="
__(
- 'You can %{gitlabLinkStart}resolve conflicts on GitLab%{gitlabLinkEnd} or %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.',
+ 'You can %{gitlabLinkStart}resolve conflicts on GitLab%{gitlabLinkEnd} or %{resolveLocallyStart}resolve them locally%{resolveLocallyEnd}.',
)
"
>
@@ -415,7 +415,7 @@ export default {
</gl-sprintf>
<gl-sprintf
v-else
- :message="__('You can %{resolveLocallyStart}resolve it locally%{resolveLocallyEnd}.')"
+ :message="__('You can %{resolveLocallyStart}resolve them locally%{resolveLocallyEnd}.')"
>
<template #resolveLocally="{ content }">
<gl-button
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index 7732badde34..479853caae3 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -57,21 +57,32 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
export const addCommentTooltip = (line) => {
let tooltip;
- if (!line) return tooltip;
+ if (!line) {
+ return tooltip;
+ }
tooltip = __('Add a comment to this line or drag for multiple lines');
- const brokenSymlinks = line.commentsDisabled;
- if (brokenSymlinks) {
- if (brokenSymlinks.wasSymbolic || brokenSymlinks.isSymbolic) {
+ if (!line.problems) {
+ return tooltip;
+ }
+
+ const { brokenSymlink, brokenLineCode, fileOnlyMoved } = line.problems;
+
+ if (brokenSymlink) {
+ if (brokenSymlink.wasSymbolic || brokenSymlink.isSymbolic) {
tooltip = __(
- 'Commenting on symbolic links that replace or are replaced by files is currently not supported.',
+ 'Commenting on symbolic links that replace or are replaced by files is not supported',
);
- } else if (brokenSymlinks.wasReal || brokenSymlinks.isReal) {
+ } else if (brokenSymlink.wasReal || brokenSymlink.isReal) {
tooltip = __(
- 'Commenting on files that replace or are replaced by symbolic links is currently not supported.',
+ 'Commenting on files that replace or are replaced by symbolic links is not supported',
);
}
+ } else if (fileOnlyMoved) {
+ tooltip = __('Commenting on files that are only moved or renamed is not supported');
+ } else if (brokenLineCode) {
+ tooltip = __('Commenting on this line is not supported');
}
return tooltip;
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 5234be44b05..c73012527a2 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,7 +5,7 @@ import {
historyPushState,
scrollToElement,
} from '~/lib/utils/common_utils';
-import { createAlert } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
@@ -229,9 +229,17 @@ export const fetchDiffFilesMeta = ({ commit, state }) => {
return data;
})
- .catch(() => worker.terminate());
-};
+ .catch((error) => {
+ worker.terminate();
+ if (error.response.status === httpStatusCodes.NOT_FOUND) {
+ createAlert({
+ message: __('Building your merge request. Wait a few moments, then refresh this page.'),
+ variant: VARIANT_WARNING,
+ });
+ }
+ });
+};
export const fetchCoverageFiles = ({ commit, state }) => {
const coveragePoll = new Poll({
resource: {
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index cf86ebea4a9..0519ca3d715 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -324,15 +324,24 @@ function cleanRichText(text) {
}
function prepareLine(line, file) {
+ const problems = {
+ brokenSymlink: file.brokenSymlink,
+ brokenLineCode: !line.line_code,
+ fileOnlyMoved: file.renamed_file && file.added_lines === 0 && file.removed_lines === 0,
+ };
+
if (!line.alreadyPrepared) {
Object.assign(line, {
- commentsDisabled: file.brokenSymlink,
+ commentsDisabled: Boolean(
+ problems.brokenSymlink || problems.fileOnlyMoved || problems.brokenLineCode,
+ ),
rich_text: cleanRichText(line.rich_text),
discussionsExpanded: true,
discussions: [],
hasForm: false,
text: undefined,
alreadyPrepared: true,
+ problems,
});
}
}
diff --git a/app/assets/javascripts/diffs/utils/tree_worker_utils.js b/app/assets/javascripts/diffs/utils/tree_worker_utils.js
index 985e75d1a17..a90c1a5c64e 100644
--- a/app/assets/javascripts/diffs/utils/tree_worker_utils.js
+++ b/app/assets/javascripts/diffs/utils/tree_worker_utils.js
@@ -62,10 +62,15 @@ export const generateTreeList = (files) => {
const split = file.new_path.split('/');
split.forEach((name, i) => {
- const parent = acc.treeEntries[split.slice(0, i).join('/')];
+ let parent = acc.treeEntries[split.slice(0, i).join('/')];
const path = `${parent ? `${parent.path}/` : ''}${name}`;
+ const child = acc.treeEntries[path];
- if (!acc.treeEntries[path]) {
+ if (parent && !parent.tree) {
+ parent = null;
+ }
+
+ if (!child || !child.tree) {
const type = path === file.new_path ? 'blob' : 'tree';
acc.treeEntries[path] = {
key: path,
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
index 83dd4b0a124..941c42f75eb 100644
--- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js
+++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { memoize, throttle } from 'lodash';
import createEventHub from '~/helpers/event_hub_factory';
@@ -34,7 +33,6 @@ class DirtySubmitForm {
this.form.addEventListener('input', throttledUpdateDirtyInput);
this.form.addEventListener('change', throttledUpdateDirtyInput);
- $(this.form).on('change.select2', throttledUpdateDirtyInput);
this.form.addEventListener('submit', (event) => this.formSubmit(event));
}
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index 999e91eed19..dd4a7a689d7 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -1,4 +1,4 @@
-import { KeyMod, KeyCode } from 'monaco-editor';
+import { KeyMod, KeyCode, Emitter } from 'monaco-editor';
import { debounce } from 'lodash';
import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
import { createAlert } from '~/flash';
@@ -56,6 +56,7 @@ export class EditorMarkdownPreviewExtension {
layoutChangeListener: undefined,
path: setupOptions.previewMarkdownPath,
actionShowPreviewCondition: instance.createContextKey('toggleLivePreview', true),
+ eventEmitter: new Emitter(),
};
this.toolbarButtons = [];
@@ -71,6 +72,8 @@ export class EditorMarkdownPreviewExtension {
EditorMarkdownPreviewExtension.resizePreviewLayout(instance, newWidth);
}
});
+
+ this.preview.eventEmitter.event(this.togglePreview.bind(this, instance));
}
onBeforeUnuse(instance) {
@@ -187,6 +190,31 @@ export class EditorMarkdownPreviewExtension {
});
}
+ togglePreview(instance) {
+ if (!this.preview?.el) {
+ this.preview.el = setupDomElement({ injectToEl: instance.getDomNode().parentElement });
+ }
+ this.togglePreviewLayout(instance);
+ this.togglePreviewPanel(instance);
+
+ this.preview.actionShowPreviewCondition.set(!this.preview.actionShowPreviewCondition.get());
+
+ if (!this.preview?.shown) {
+ this.preview.modelChangeListener = instance.onDidChangeModelContent(
+ debounce(this.fetchPreview.bind(this, instance), EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY),
+ );
+ } else {
+ this.preview.modelChangeListener.dispose();
+ }
+
+ this.preview.shown = !this.preview?.shown;
+ if (instance.toolbar) {
+ instance.toolbar.updateItem(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, {
+ selected: this.preview.shown,
+ });
+ }
+ }
+
provides() {
return {
markdownPreview: this.preview,
@@ -195,33 +223,7 @@ export class EditorMarkdownPreviewExtension {
setupPreviewAction: (instance) => this.setupPreviewAction(instance),
- togglePreview: (instance) => {
- if (!this.preview?.el) {
- this.preview.el = setupDomElement({ injectToEl: instance.getDomNode().parentElement });
- }
- this.togglePreviewLayout(instance);
- this.togglePreviewPanel(instance);
-
- this.preview.actionShowPreviewCondition.set(!this.preview.actionShowPreviewCondition.get());
-
- if (!this.preview?.shown) {
- this.preview.modelChangeListener = instance.onDidChangeModelContent(
- debounce(
- this.fetchPreview.bind(this, instance),
- EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
- ),
- );
- } else {
- this.preview.modelChangeListener.dispose();
- }
-
- this.preview.shown = !this.preview?.shown;
- if (instance.toolbar) {
- instance.toolbar.updateItem(EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, {
- selected: this.preview.shown,
- });
- }
- },
+ togglePreview: (instance) => this.togglePreview(instance),
};
}
}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index e56932a9a31..45f063a2048 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -103,7 +103,9 @@
"workflow": {
"type": "object",
"properties": {
- "name": { "$ref": "#/definitions/workflowName" },
+ "name": {
+ "$ref": "#/definitions/workflowName"
+ },
"rules": {
"type": "array",
"items": {
@@ -130,7 +132,7 @@
"$ref": "#/definitions/exists"
},
"variables": {
- "$ref": "#/definitions/variables"
+ "$ref": "#/definitions/rulesVariables"
},
"when": {
"type": "string",
@@ -688,7 +690,7 @@
"$ref": "#/definitions/exists"
},
"variables": {
- "$ref": "#/definitions/variables"
+ "$ref": "#/definitions/rulesVariables"
},
"when": {
"$ref": "#/definitions/when"
@@ -742,6 +744,10 @@
"description": {
"type": "string",
"markdownDescription": "Explains what the variable is used for, what the acceptable values are. Variables with `description` are prefilled when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesdescription)."
+ },
+ "expand": {
+ "type": "boolean",
+ "markdownDescription": "If the variable is expandable or not. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesexpand)."
}
},
"additionalProperties": false
@@ -751,6 +757,49 @@
"additionalProperties": false
}
},
+ "jobVariables": {
+ "markdownDescription": "Defines variables for a job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "oneOf": [
+ {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "expand": {
+ "type": "boolean",
+ "markdownDescription": "Defines if the variable is expandable or not. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesexpand)."
+ }
+ },
+ "additionalProperties": false
+ }
+ ]
+ },
+ "additionalProperties": false
+ }
+ },
+ "rulesVariables": {
+ "markdownDescription": "Defines variables for a rule result. [Learn More](https://docs.gitlab.com/ee/ci/yaml/index.html#rulesvariables).",
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ "additionalProperties": false
+ }
+ },
"if": {
"type": "string",
"markdownDescription": "Expression to evaluate whether additional attributes should be provided to the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rulesif)."
@@ -793,19 +842,6 @@
"type": "string"
}
},
- "variables": {
- "markdownDescription": "Defines environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
- "type": "object",
- "patternProperties": {
- ".*": {
- "type": [
- "string",
- "number"
- ]
- },
- "additionalProperties": false
- }
- },
"timeout": {
"type": "string",
"markdownDescription": "Allows you to configure a timeout for a specific job (e.g. `1 minute`, `1h 30m 12s`). [Learn More](https://docs.gitlab.com/ee/ci/yaml/index.html#timeout).",
@@ -858,137 +894,77 @@
]
},
"when": {
- "markdownDescription": "Describes the conditions for when to run the job. Defaults to 'on_success'.",
+ "markdownDescription": "Describes the conditions for when to run the job. Defaults to 'on_success'. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when).",
"default": "on_success",
- "oneOf": [
- {
- "enum": [
- "on_success"
- ],
- "description": "Execute job only when all jobs from prior stages succeed."
- },
- {
- "enum": [
- "on_failure"
- ],
- "description": "Execute job when at least one job from prior stages fails."
- },
- {
- "enum": [
- "always"
- ],
- "description": "Execute job regardless of the status from prior stages."
- },
- {
- "enum": [
- "manual"
- ],
- "markdownDescription": "Execute the job manually from Gitlab UI or API. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
- },
- {
- "enum": [
- "delayed"
- ],
- "markdownDescription": "Execute a job after the time limit in 'start_in' expires. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
- },
- {
- "enum": [
- "never"
- ],
- "description": "Never execute the job."
- }
+ "type": "string",
+ "enum": [
+ "on_success",
+ "on_failure",
+ "always",
+ "never",
+ "manual",
+ "delayed"
]
},
"cache": {
+ "markdownDescription": "Use `cache` to specify a list of files and directories to cache between jobs. You can only use paths that are in the local working copy. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cache)",
"properties": {
- "when": {
- "markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).",
- "default": "on_success",
- "oneOf": [
- {
- "enum": [
- "on_success"
- ],
- "description": "Save the cache only when the job succeeds."
- },
- {
- "enum": [
- "on_failure"
- ],
- "description": "Save the cache only when the job fails. "
- },
- {
- "enum": [
- "always"
- ],
- "description": "Always save the cache. "
- }
- ]
- }
- }
- },
- "cache_entry": {
- "type": "object",
- "description": "Specify files or directories to cache between jobs. Can be set globally or per job.",
- "additionalProperties": false,
- "properties": {
- "paths": {
- "type": "array",
- "description": "List of files or paths to cache.",
- "items": {
- "type": "string"
- }
- },
"key": {
+ "markdownDescription": "Use the `cache:key` keyword to give each cache a unique identifying key. All jobs that use the same cache key use the same cache, including in different pipelines. Must be used with `cache:path`, or nothing is cached. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachekey).",
"oneOf": [
{
"type": "string",
- "description": "Unique cache ID, to allow e.g. specific branch or job cache. Environment variables can be used to set up unique keys (e.g. \"$CI_COMMIT_REF_SLUG\" for per branch cache)."
+ "pattern": "^(?!.*\\/)^(.*[^.]+.*)$"
},
{
"type": "object",
- "description": "When you include cache:key:files, you must also list the project files that will be used to generate the key, up to a maximum of two files. The cache key will be a SHA checksum computed from the most recent commits (up to two, if two files are listed) that changed the given files.",
"properties": {
"files": {
+ "markdownDescription": "Use the `cache:key:files` keyword to generate a new key when one or two specific files change. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachekeyfiles)",
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 2
+ },
+ "prefix": {
+ "markdownDescription": "Use `cache:key:prefix` to combine a prefix with the SHA computed for `cache:key:files`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachekeyprefix)",
+ "type": "string"
}
}
}
]
},
- "untracked": {
- "type": "boolean",
- "description": "Set to `true` to cache untracked files.",
- "default": false
+ "paths": {
+ "type": "array",
+ "markdownDescription": "Use the `cache:paths` keyword to choose which files or directories to cache. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachepaths)",
+ "items": {
+ "type": "string"
+ }
},
"policy": {
"type": "string",
- "description": "Determines the strategy for downloading and updating the cache.",
+ "markdownDescription": "Determines the strategy for downloading and updating the cache. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachepolicy)",
"default": "pull-push",
- "oneOf": [
- {
- "enum": [
- "pull"
- ],
- "description": "Pull will download cache but skip uploading after job completes."
- },
- {
- "enum": [
- "push"
- ],
- "description": "Push will skip downloading cache and always recreate cache after job completes."
- },
- {
- "enum": [
- "pull-push"
- ],
- "description": "Pull-push will both download cache at job start and upload cache on job success."
- }
+ "enum": [
+ "pull",
+ "push",
+ "pull-push"
+ ]
+ },
+ "untracked": {
+ "type": "boolean",
+ "markdownDescription": "Use `untracked: true` to cache all files that are untracked in your Git repository. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cacheuntracked)",
+ "default": false
+ },
+ "when": {
+ "markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).",
+ "default": "on_success",
+ "enum": [
+ "on_success",
+ "on_failure",
+ "always"
]
}
}
@@ -1228,7 +1204,7 @@
"$ref": "#/definitions/rules"
},
"variables": {
- "$ref": "#/definitions/variables"
+ "$ref": "#/definitions/jobVariables"
},
"cache": {
"$ref": "#/definitions/cache"
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index c7e024aadec..74eef50ebaf 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -57,7 +57,8 @@ export default {
this.isLoading = true;
if (this.graphql) {
- this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
+ await this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
+ this.isLoading = false;
} else {
eventHub.$emit('postAction', { endpoint: action.playPath });
}
diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue
index f7f0cf4cb8d..f7a853f3128 100644
--- a/app/assets/javascripts/environments/components/environment_rollback.vue
+++ b/app/assets/javascripts/environments/components/environment_rollback.vue
@@ -51,17 +51,20 @@ export default {
methods: {
onClick() {
+ const rollbackEnvironmentData = {
+ ...this.environment,
+ retryUrl: this.retryUrl,
+ isLastDeployment: this.isLastDeployment,
+ };
if (this.graphql) {
this.$apollo.mutate({
mutation: setEnvironmentToRollback,
- variables: { environment: this.environment },
+ variables: {
+ environment: rollbackEnvironmentData,
+ },
});
} else {
- eventHub.$emit('requestRollbackEnvironment', {
- ...this.environment,
- retryUrl: this.retryUrl,
- isLastDeployment: this.isLastDeployment,
- });
+ eventHub.$emit('requestRollbackEnvironment', rollbackEnvironmentData);
}
},
},
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql
index f7586e27665..84c6998f234 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_to_rollback.query.graphql
@@ -3,5 +3,6 @@ query environmentToRollback {
id
name
lastDeployment
+ retryUrl
}
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 26507a85fa8..fe580aab108 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -1,3 +1,4 @@
+import { ACTIVE_AND_BLOCKED_USER_STATES } from '~/users_select/constants';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
import DropdownAjaxFilter from './dropdown_ajax_filter';
@@ -14,7 +15,7 @@ export default class DropdownUser extends DropdownAjaxFilter {
return {
...super.ajaxFilterConfig(),
params: {
- active: true,
+ states: ACTIVE_AND_BLOCKED_USER_STATES,
group_id: this.getGroupId(),
project_id: this.getProjectId(),
current_user: true,
diff --git a/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js b/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
index d0f2d205bb6..d6abab4c9ed 100644
--- a/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/droplab/plugins/ajax_filter.js
@@ -1,6 +1,7 @@
/* eslint-disable */
import AjaxCache from '~/lib/utils/ajax_cache';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
const AjaxFilter = {
init: function (hook) {
@@ -62,7 +63,7 @@ const AjaxFilter = {
this.loading = true;
var params = config.params || {};
params[config.searchKey] = searchValue;
- var url = config.endpoint + this.buildParams(params);
+ var url = mergeUrlParams(params, config.endpoint, { spreadArrays: true });
return AjaxCache.retrieve(url)
.then((data) => {
this._loadData(data, config);
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index acb7449f830..d6e7887f93f 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -1,10 +1,18 @@
import { flattenDeep } from 'lodash';
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
+ TOKEN_TITLE_RELEASE,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
export const tokenKeys = [
{
- formattedKey: __('Author'),
+ formattedKey: TOKEN_TITLE_AUTHOR,
key: 'author',
type: 'string',
param: 'username',
@@ -13,7 +21,7 @@ export const tokenKeys = [
tag: '@author',
},
{
- formattedKey: s__('SearchToken|Assignee'),
+ formattedKey: TOKEN_TITLE_ASSIGNEE,
key: 'assignee',
type: 'string',
param: 'username',
@@ -22,7 +30,7 @@ export const tokenKeys = [
tag: '@assignee',
},
{
- formattedKey: __('Milestone'),
+ formattedKey: TOKEN_TITLE_MILESTONE,
key: 'milestone',
type: 'string',
param: 'title',
@@ -31,7 +39,7 @@ export const tokenKeys = [
tag: '%milestone',
},
{
- formattedKey: __('Release'),
+ formattedKey: TOKEN_TITLE_RELEASE,
key: 'release',
type: 'string',
param: 'tag',
@@ -40,7 +48,7 @@ export const tokenKeys = [
tag: __('tag name'),
},
{
- formattedKey: __('Label'),
+ formattedKey: TOKEN_TITLE_LABEL,
key: 'label',
type: 'array',
param: 'name[]',
@@ -53,7 +61,7 @@ export const tokenKeys = [
if (gon.current_user_id) {
// Appending tokenkeys only logged-in
tokenKeys.push({
- formattedKey: __('My-Reaction'),
+ formattedKey: TOKEN_TITLE_MY_REACTION,
key: 'my-reaction',
type: 'string',
param: 'emoji',
@@ -65,7 +73,7 @@ if (gon.current_user_id) {
export const alternativeTokenKeys = [
{
- formattedKey: __('Label'),
+ formattedKey: TOKEN_TITLE_LABEL,
key: 'label',
type: 'string',
param: 'name',
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 5665231e613..dc6c4642e94 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -109,6 +109,7 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
*
* @param {object} options - Options to control the flash message
* @param {string} options.message - Alert message text
+ * @param {string} [options.title] - Alert title
* @param {VARIANT_SUCCESS|VARIANT_WARNING|VARIANT_DANGER|VARIANT_INFO|VARIANT_TIP} [options.variant] - Which GlAlert variant to use; it defaults to VARIANT_DANGER.
* @param {object} [options.parent] - Reference to parent element under which alert needs to appear. Defaults to `document`.
* @param {Function} [options.onDismiss] - Handler to call when this alert is dismissed.
@@ -126,6 +127,7 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
*/
const createAlert = function createAlert({
message,
+ title,
variant = VARIANT_DANGER,
parent = document,
containerSelector = '.flash-container',
@@ -183,6 +185,7 @@ const createAlert = function createAlert({
GlAlert,
{
props: {
+ title,
dismissible: true,
dismissLabel: __('Dismiss'),
variant,
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 01d218438cf..49c47e9d778 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -20,7 +20,12 @@ const MERGEREQUESTS_ALIAS = 'mergerequests';
const LABELS_ALIAS = 'labels';
const SNIPPETS_ALIAS = 'snippets';
const CONTACTS_ALIAS = 'contacts';
+
export const AT_WHO_ACTIVE_CLASS = 'at-who-active';
+export const CONTACT_STATE_ACTIVE = 'active';
+export const CONTACTS_ADD_COMMAND = '/add_contacts';
+export const CONTACTS_REMOVE_COMMAND = '/remove_contacts';
+
/**
* Escapes user input before we pass it to at.js, which
* renders it as HTML in the autocomplete dropdown.
@@ -666,6 +671,9 @@ class GfmAutoComplete {
}
setupContacts($input) {
+ const fetchData = this.fetchData.bind(this);
+ let command = '';
+
$input.atwho({
at: '[contact:',
suffix: ']',
@@ -694,9 +702,44 @@ class GfmAutoComplete {
firstName: m.first_name,
lastName: m.last_name,
search: `${m.email}`,
+ state: m.state,
+ set: m.set,
};
});
},
+ matcher(flag, subtext) {
+ const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
+
+ command = subtextNodes.find((node) => {
+ if (node === CONTACTS_ADD_COMMAND || node === CONTACTS_REMOVE_COMMAND) {
+ return node;
+ }
+ return null;
+ });
+
+ const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
+ return match?.length ? match[1] : null;
+ },
+ filter(query, data, searchKey) {
+ if (GfmAutoComplete.isLoading(data)) {
+ fetchData(this.$inputor, this.at);
+ return data;
+ }
+
+ if (data === GfmAutoComplete.defaultLoadingData) {
+ return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
+ }
+
+ 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) {
+ // Return contacts already on the issue
+ return data.filter((contact) => contact.set);
+ }
+
+ return data;
+ },
},
});
showAndHideHelper($input, CONTACTS_ALIAS);
diff --git a/app/assets/javascripts/gitlab_version_check.js b/app/assets/javascripts/gitlab_version_check.js
deleted file mode 100644
index 2892aded7c5..00000000000
--- a/app/assets/javascripts/gitlab_version_check.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
-
-const mountGitlabVersionCheck = (el) => {
- const { size } = el.dataset;
-
- return new Vue({
- el,
- render(createElement) {
- return createElement(GitlabVersionCheck, {
- props: {
- size,
- },
- });
- },
- });
-};
-
-export default () =>
- [...document.querySelectorAll('.js-gitlab-version-check')].map(mountGitlabVersionCheck);
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
new file mode 100644
index 00000000000..1536a9a525b
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/components/gitlab_version_check_badge.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import { STATUS_TYPES, UPGRADE_DOCS_URL } from '../constants';
+
+export default {
+ name: 'GitlabVersionCheckBadge',
+ components: {
+ GlBadge,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ actionable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ if (this.status === STATUS_TYPES.SUCCESS) {
+ return s__('VersionCheck|Up to date');
+ } else if (this.status === STATUS_TYPES.WARNING) {
+ return s__('VersionCheck|Update available');
+ } else if (this.status === STATUS_TYPES.DANGER) {
+ return s__('VersionCheck|Update ASAP');
+ }
+
+ return null;
+ },
+ badgeUrl() {
+ return this.actionable ? UPGRADE_DOCS_URL : null;
+ },
+ },
+ mounted() {
+ this.track('render', {
+ label: 'version_badge',
+ property: this.title,
+ });
+ },
+ methods: {
+ onClick() {
+ if (!this.actionable) return;
+
+ this.track('click_link', {
+ label: 'version_badge',
+ property: this.title,
+ });
+ },
+ },
+ UPGRADE_DOCS_URL,
+};
+</script>
+
+<template>
+ <!-- TODO: remove the span element once bootstrap-vue is updated to version 2.21.1 -->
+ <!-- TODO: https://github.com/bootstrap-vue/bootstrap-vue/issues/6219 -->
+ <span data-testid="badge-click-wrapper" @click="onClick">
+ <gl-badge :href="badgeUrl" class="version-check-badge" :variant="status" :size="size">{{
+ title
+ }}</gl-badge>
+ </span>
+</template>
diff --git a/app/assets/javascripts/gitlab_version_check/constants.js b/app/assets/javascripts/gitlab_version_check/constants.js
new file mode 100644
index 00000000000..259723a4e22
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/constants.js
@@ -0,0 +1,9 @@
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export const STATUS_TYPES = {
+ SUCCESS: 'success',
+ WARNING: 'warning',
+ DANGER: 'danger',
+};
+
+export const UPGRADE_DOCS_URL = helpPagePath('update/index');
diff --git a/app/assets/javascripts/gitlab_version_check/index.js b/app/assets/javascripts/gitlab_version_check/index.js
new file mode 100644
index 00000000000..203ce10ef57
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/index.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import * as Sentry from '@sentry/browser';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
+import GitlabVersionCheckBadge from './components/gitlab_version_check_badge.vue';
+
+const mountGitlabVersionCheckBadge = ({ el, status }) => {
+ const { size } = el.dataset;
+ const actionable = parseBoolean(el.dataset.actionable);
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(GitlabVersionCheckBadge, {
+ props: {
+ size,
+ actionable,
+ status,
+ },
+ });
+ },
+ });
+};
+
+export default async () => {
+ const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
+
+ // If there are no version check elements, exit out
+ if (versionCheckBadges?.length <= 0) {
+ return null;
+ }
+
+ const status = await axios
+ .get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
+ .then((res) => {
+ return res.data?.severity;
+ })
+ .catch((e) => {
+ Sentry.captureException(e);
+ return null;
+ });
+
+ // If we don't have a status there is nothing to render
+ if (status) {
+ return versionCheckBadges.map((el) => mountGitlabVersionCheckBadge({ el, status }));
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index eec7a138ea7..28aa9906116 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -15,9 +15,14 @@ export default class GlFieldErrors {
initValidators() {
// register selectors here as needed
- const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]']
- .map((selector) => `input${selector}`)
- .join(',');
+ const validateSelectors = [
+ 'input:text',
+ 'input:password',
+ 'input[type=email]',
+ 'input[type=url]',
+ 'input[type=number]',
+ 'textarea',
+ ].join(',');
this.state.inputs = this.form
.find(validateSelectors)
diff --git a/app/assets/javascripts/google_cloud/service_accounts/list.vue b/app/assets/javascripts/google_cloud/service_accounts/list.vue
index 4b580c594f5..c9d9a9a3e8c 100644
--- a/app/assets/javascripts/google_cloud/service_accounts/list.vue
+++ b/app/assets/javascripts/google_cloud/service_accounts/list.vue
@@ -1,7 +1,10 @@
<script>
import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf, GlTable } from '@gitlab/ui';
+import { setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+const GOOGLE_CONSOLE_URL = 'https://console.cloud.google.com/iam-admin/serviceaccounts';
+
export default {
components: { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf, GlTable },
props: {
@@ -40,6 +43,12 @@ export default {
'Enhance security by storing service account keys in secret managers - learn more about %{docLinkStart}secret management with GitLab%{docLinkEnd}',
),
},
+ methods: {
+ gcpProjectUrl(id) {
+ return setUrlParams({ project: id }, GOOGLE_CONSOLE_URL);
+ },
+ },
+ GOOGLE_CONSOLE_URL,
};
</script>
@@ -59,6 +68,9 @@ export default {
<p>{{ $options.i18n.serviceAccountsDescription }}</p>
<gl-table :items="list" :fields="$options.tableFields">
+ <template #cell(gcp_project)="{ value }">
+ <gl-link :href="gcpProjectUrl(value)">{{ value }}</gl-link>
+ </template>
<template #cell(service_account_exists)="{ value }">
{{ value ? $options.i18n.found : $options.i18n.notFound }}
</template>
diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js
index 5b0bcfa963b..98c9db1fc9a 100644
--- a/app/assets/javascripts/google_tag_manager/index.js
+++ b/app/assets/javascripts/google_tag_manager/index.js
@@ -40,6 +40,11 @@ const generateProductInfo = (sku, quantity) => {
};
const isSupported = () => Boolean(window.dataLayer) && gon.features?.gitlabGtmDatalayer;
+// gon.features.gitlabGtmDatalayer is set by writing
+// `push_frontend_feature_flag(:gitlab_gtm_datalayer, type: :ops)`
+// to the appropriate controller
+// window.dataLayer is set by adding partials to the appropriate view found in
+// views/layouts/_google_tag_manager_body.html.haml and _google_tag_manager_head.html.haml
const pushEvent = (event, args = {}) => {
if (!window.dataLayer) {
@@ -287,3 +292,27 @@ export const trackCompanyForm = (aboutYourCompanyType) => {
pushEvent('aboutYourCompanyFormSubmit', { aboutYourCompanyType });
};
+
+export const saasTrialWelcome = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const saasTrialWelcomeButton = document.querySelector('.js-trial-welcome-btn');
+
+ saasTrialWelcomeButton?.addEventListener('click', () => {
+ pushEvent('saasTrialWelcome');
+ });
+};
+
+export const saasTrialContinuousOnboarding = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const getStartedButton = document.querySelector('.js-get-started-btn');
+
+ getStartedButton?.addEventListener('click', () => {
+ pushEvent('saasTrialContinuousOnboarding');
+ });
+};
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index 3b737dfff33..15e7ef7d62c 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -1,16 +1,18 @@
import produce from 'immer';
import VueApollo from 'vue-apollo';
+import { defaultDataIdFromObject } from '@apollo/client/core';
import { concatPagination } from '@apollo/client/utilities';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
-import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants';
-export const temporaryConfig = {
+export const config = {
typeDefs,
cacheConfig: {
- possibleTypes: {
- LocalWorkItemWidget: ['LocalWorkItemMilestone'],
+ // included temporarily until Vuex is removed from boards app
+ dataIdFromObject: (object) => {
+ // eslint-disable-next-line no-underscore-dangle
+ return object.__typename === 'BoardList' ? object.iid : defaultDataIdFromObject(object);
},
typePolicies: {
Project: {
@@ -22,35 +24,15 @@ export const temporaryConfig = {
},
WorkItem: {
fields: {
- mockWidgets: {
- read(widgets) {
- return (
- widgets || [
- {
- __typename: 'LocalWorkItemMilestone',
- type: WIDGET_TYPE_MILESTONE,
- nodes: [
- {
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/30',
- title: 'v4.0',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- __typename: 'Milestone',
- },
- ],
- },
- ]
- );
- },
- },
widgets: {
merge(existing = [], incoming) {
if (existing.length === 0) {
return incoming;
}
return existing.map((existingWidget) => {
- const incomingWidget = incoming.find((w) => w.type === existingWidget.type);
+ const incomingWidget = incoming.find(
+ (w) => w.type && w.type === existingWidget.type,
+ );
return incomingWidget || existingWidget;
});
},
@@ -78,7 +60,7 @@ export const resolvers = {
},
};
-export const defaultClient = createDefaultClient(resolvers, temporaryConfig);
+export const defaultClient = createDefaultClient(resolvers, config);
export const apolloProvider = new VueApollo({
defaultClient,
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 545c150e536..e8b0174b8f6 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -9,6 +9,10 @@
"CiManualVariable",
"CiProjectVariable"
],
+ "CommitSignature": [
+ "GpgSignature",
+ "X509Signature"
+ ],
"CurrentUserTodos": [
"BoardEpic",
"Design",
@@ -143,8 +147,9 @@
"WorkItemWidgetHierarchy",
"WorkItemWidgetIteration",
"WorkItemWidgetLabels",
+ "WorkItemWidgetMilestone",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetStatus",
"WorkItemWidgetWeight"
]
-}
+} \ No newline at end of file
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index d0c5846ac88..46ab30367a0 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -15,6 +15,7 @@ import eventHub from '../event_hub';
import GroupsApp from './app.vue';
const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
+const MIN_SEARCH_LENGTH = 3;
export default {
components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
@@ -136,7 +137,9 @@ export default {
handleSearchInput(value) {
this.search = value;
- this.debouncedSearch();
+ if (!this.search || this.search.length >= MIN_SEARCH_LENGTH) {
+ this.debouncedSearch();
+ }
},
debouncedSearch: debounce(async function debouncedSearch() {
this.handleSearchOrSortChange();
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index e28459811d7..15a193f7cb8 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -1,29 +1,24 @@
<script>
-import { GlFormGroup } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import { getGroupTransferLocations } from '~/api/groups_api';
export const i18n = {
confirmationMessage: __(
'You are going to transfer %{group_name} to another namespace. Are you ABSOLUTELY sure?',
),
emptyNamespaceTitle: __('No parent group'),
- dropdownTitle: s__('GroupSettings|Select parent group'),
+ dropdownLabel: s__('GroupSettings|Select parent group'),
};
export default {
name: 'TransferGroupForm',
components: {
ConfirmDanger,
- GlFormGroup,
- NamespaceSelect,
+ TransferLocations,
},
props: {
- groupNamespaces: {
- type: Array,
- required: true,
- },
isPaidGroup: {
type: Boolean,
required: true,
@@ -39,36 +34,41 @@ export default {
},
data() {
return {
- selectedId: null,
+ selectedTransferLocation: null,
};
},
computed: {
disableSubmitButton() {
- return this.isPaidGroup || !this.selectedId;
+ return this.isPaidGroup || !this.selectedTransferLocation;
+ },
+ selectedTransferLocationId() {
+ return this.selectedTransferLocation?.id;
},
},
methods: {
- handleSelected({ id }) {
- this.selectedId = id;
- },
+ getGroupTransferLocations,
},
i18n,
+ additionalDropdownItems: [
+ {
+ id: -1,
+ humanName: i18n.emptyNamespaceTitle,
+ },
+ ],
};
</script>
<template>
<div>
- <gl-form-group v-if="!isPaidGroup">
- <namespace-select
- :default-text="$options.i18n.dropdownTitle"
- :group-namespaces="groupNamespaces"
- :empty-namespace-title="$options.i18n.emptyNamespaceTitle"
- :include-headers="false"
- include-empty-namespace
- data-testid="transfer-group-namespace-select"
- @select="handleSelected"
- />
- <input type="hidden" name="new_parent_group_id" :value="selectedId" />
- </gl-form-group>
+ <input type="hidden" name="new_parent_group_id" :value="selectedTransferLocationId" />
+ <transfer-locations
+ v-if="!isPaidGroup"
+ v-model="selectedTransferLocation"
+ :show-user-transfer-locations="false"
+ data-testid="transfer-group-namespace"
+ :group-transfer-locations-api-method="getGroupTransferLocations"
+ :additional-dropdown-items="$options.additionalDropdownItems"
+ :label="$options.i18n.dropdownLabel"
+ />
<confirm-danger
:disabled="disableSubmitButton"
:phrase="confirmationPhrase"
diff --git a/app/assets/javascripts/groups/init_transfer_group_form.js b/app/assets/javascripts/groups/init_transfer_group_form.js
index f055b926918..503dad673dd 100644
--- a/app/assets/javascripts/groups/init_transfer_group_form.js
+++ b/app/assets/javascripts/groups/init_transfer_group_form.js
@@ -1,42 +1,38 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { sprintf } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
import TransferGroupForm, { i18n } from './components/transfer_group_form.vue';
-const prepareGroups = (rawGroups) => {
- if (!rawGroups) {
- return [];
- }
-
- return JSON.parse(rawGroups).map(({ id, text: humanName }) => ({
- id,
- humanName,
- }));
-};
-
export default () => {
const el = document.querySelector('.js-transfer-group-form');
if (!el) {
return false;
}
+ Vue.use(VueApollo);
+
const {
targetFormId = null,
buttonText: confirmButtonText = '',
groupName = '',
- parentGroups,
+ groupId: resourceId,
isPaidGroup,
} = el.dataset;
return new Vue({
el,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient(),
+ }),
provide: {
confirmDangerMessage: sprintf(i18n.confirmationMessage, { group_name: groupName }),
+ resourceId,
},
render(createElement) {
return createElement(TransferGroupForm, {
props: {
- groupNamespaces: prepareGroups(parentGroups),
isPaidGroup: parseBoolean(isPaidGroup),
confirmButtonText,
confirmationPhrase: groupName,
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
new file mode 100644
index 00000000000..e0c8ce36e3c
--- /dev/null
+++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
@@ -0,0 +1,282 @@
+<script>
+import {
+ GlAlert,
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { s__, __ } from '~/locale';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import currentUserNamespace from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+
+export const i18n = {
+ SELECT_A_NAMESPACE: __('Select a new namespace'),
+ GROUPS: __('Groups'),
+ USERS: __('Users'),
+ ERROR_MESSAGE: s__(
+ 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
+ ),
+ ALERT_DISMISS_LABEL: __('Dismiss'),
+};
+
+export default {
+ name: 'TransferLocations',
+ components: {
+ GlAlert,
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ },
+ inject: ['resourceId'],
+ props: {
+ value: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ groupTransferLocationsApiMethod: {
+ type: Function,
+ required: true,
+ },
+ showUserTransferLocations: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ additionalDropdownItems: {
+ type: Array,
+ required: false,
+ default() {
+ return [];
+ },
+ },
+ label: {
+ type: String,
+ required: false,
+ default: i18n.SELECT_A_NAMESPACE,
+ },
+ },
+ initialTransferLocationsLoaded: false,
+ data() {
+ return {
+ searchTerm: '',
+ userTransferLocations: [],
+ groupTransferLocations: [],
+ filteredAdditionalDropdownItems: this.additionalDropdownItems,
+ isLoading: false,
+ isSearchLoading: false,
+ hasError: false,
+ page: 1,
+ totalPages: 1,
+ };
+ },
+ computed: {
+ hasUserTransferLocations() {
+ return this.userTransferLocations.length;
+ },
+ hasGroupTransferLocations() {
+ return this.groupTransferLocations.length;
+ },
+ selectedText() {
+ return this.value?.humanName || this.label;
+ },
+ hasNextPageOfGroups() {
+ return this.page < this.totalPages;
+ },
+ showAdditionalDropdownItems() {
+ return !this.isLoading && this.filteredAdditionalDropdownItems.length;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.page = 1;
+
+ this.debouncedSearch();
+ },
+ },
+ methods: {
+ handleSelect(item) {
+ this.searchTerm = '';
+ this.$emit('input', item);
+ },
+ async handleShow() {
+ if (this.$options.initialTransferLocationsLoaded) {
+ return;
+ }
+
+ this.isLoading = true;
+
+ [this.groupTransferLocations, this.userTransferLocations] = await Promise.all([
+ this.getGroupTransferLocations(),
+ this.getUserTransferLocations(),
+ ]);
+
+ this.isLoading = false;
+ this.$options.initialTransferLocationsLoaded = true;
+ },
+ async getGroupTransferLocations() {
+ try {
+ const {
+ data: groupTransferLocations,
+ headers,
+ } = await this.groupTransferLocationsApiMethod(this.resourceId, {
+ page: this.page,
+ search: this.searchTerm,
+ });
+
+ const { totalPages } = parseIntPagination(normalizeHeaders(headers));
+ this.totalPages = totalPages;
+
+ return groupTransferLocations.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }));
+ } catch {
+ this.handleError();
+
+ return [];
+ }
+ },
+ async getUserTransferLocations() {
+ if (!this.showUserTransferLocations) {
+ return [];
+ }
+
+ try {
+ const {
+ data: {
+ currentUser: { namespace },
+ },
+ } = await this.$apollo.query({
+ query: currentUserNamespace,
+ });
+
+ if (!namespace) {
+ return [];
+ }
+
+ return [
+ {
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
+ },
+ ];
+ } catch {
+ this.handleError();
+
+ return [];
+ }
+ },
+ async handleLoadMoreGroups() {
+ this.isLoading = true;
+ this.page += 1;
+
+ const groupTransferLocations = await this.getGroupTransferLocations();
+ this.groupTransferLocations.push(...groupTransferLocations);
+
+ this.isLoading = false;
+ },
+ debouncedSearch: debounce(async function debouncedSearch() {
+ this.isSearchLoading = true;
+
+ this.groupTransferLocations = await this.getGroupTransferLocations();
+
+ this.filteredAdditionalDropdownItems = this.additionalDropdownItems.filter((dropdownItem) =>
+ dropdownItem.humanName.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ );
+
+ this.isSearchLoading = false;
+ }, DEBOUNCE_DELAY),
+ handleError() {
+ this.hasError = true;
+ },
+ handleAlertDismiss() {
+ this.hasError = false;
+ },
+ },
+ i18n,
+};
+</script>
+<template>
+ <div>
+ <gl-alert
+ v-if="hasError"
+ variant="danger"
+ :dismiss-label="$options.i18n.ALERT_DISMISS_LABEL"
+ @dismiss="handleAlertDismiss"
+ >{{ $options.i18n.ERROR_MESSAGE }}</gl-alert
+ >
+ <gl-form-group :label="label">
+ <gl-dropdown
+ :text="selectedText"
+ data-qa-selector="namespaces_list"
+ data-testid="transfer-locations-dropdown"
+ block
+ toggle-class="gl-mb-0"
+ @show="handleShow"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ v-model.trim="searchTerm"
+ :is-loading="isSearchLoading"
+ data-qa-selector="namespaces_list_search"
+ />
+ </template>
+ <template v-if="showAdditionalDropdownItems">
+ <gl-dropdown-item
+ v-for="item in filteredAdditionalDropdownItems"
+ :key="item.id"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ <gl-dropdown-divider />
+ </template>
+ <div
+ v-if="hasUserTransferLocations"
+ data-qa-selector="namespaces_list_users"
+ data-testid="user-transfer-locations"
+ >
+ <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in userTransferLocations"
+ :key="item.id"
+ data-qa-selector="namespaces_list_item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ <div
+ v-if="hasGroupTransferLocations"
+ data-qa-selector="namespaces_list_groups"
+ data-testid="group-transfer-locations"
+ >
+ <gl-dropdown-section-header v-if="showUserTransferLocations">{{
+ $options.i18n.GROUPS
+ }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in groupTransferLocations"
+ :key="item.id"
+ data-qa-selector="namespaces_list_item"
+ @click="handleSelect(item)"
+ >{{ item.humanName }}</gl-dropdown-item
+ >
+ </div>
+ <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
+ <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="handleLoadMoreGroups" />
+ </gl-dropdown>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 34e984a9bb9..fb0c47fe018 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,11 +1,38 @@
+import Vue from 'vue';
import $ from 'jquery';
import { escape } from 'lodash';
+import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
import { groupsPath } from '~/vue_shared/components/group_select/utils';
import { __ } from '~/locale';
import Api from './api';
import { loadCSSFile } from './lib/utils/css_utils';
import { select2AxiosTransport } from './lib/utils/select2_utils';
+const initVueSelect = () => {
+ [...document.querySelectorAll('.ajax-groups-select')].forEach((el) => {
+ const { parentId: parentGroupID, groupsFilter, inputId } = el.dataset;
+
+ return new Vue({
+ el,
+ components: {
+ GroupSelect,
+ },
+ render(createElement) {
+ return createElement(GroupSelect, {
+ props: {
+ inputName: el.name,
+ initialSelection: el.value || null,
+ parentGroupID,
+ groupsFilter,
+ inputId,
+ clearable: el.classList.contains('allowClear'),
+ },
+ });
+ },
+ });
+ });
+};
+
const groupsSelect = () => {
loadCSSFile(gon.select2_css_path)
.then(() => {
@@ -84,8 +111,12 @@ const groupsSelect = () => {
export default () => {
if ($('.ajax-groups-select').length) {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(groupsSelect)
- .catch(() => {});
+ if (gon.features?.vueGroupSelect) {
+ initVueSelect();
+ } else {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(groupsSelect)
+ .catch(() => {});
+ }
}
};
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index d589f56dd7c..838debf1ceb 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -47,6 +47,7 @@ export default {
data() {
return {
loadDeferred: false,
+ skipBeforeUnload: false,
};
},
computed: {
@@ -78,9 +79,14 @@ export default {
mounted() {
window.onbeforeunload = (e) => this.onBeforeUnload(e);
+ eventHub.$on('skip-beforeunload', this.handleSkipBeforeUnload);
+
if (this.themeName)
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
},
+ destroyed() {
+ eventHub.$off('skip-beforeunload', this.handleSkipBeforeUnload);
+ },
beforeCreate() {
performanceMarkAndMeasure({
mark: WEBIDE_MARK_APP_START,
@@ -94,6 +100,11 @@ export default {
methods: {
...mapActions(['toggleFileFinder']),
onBeforeUnload(e = {}) {
+ if (this.skipBeforeUnload) {
+ this.skipBeforeUnload = false;
+ return undefined;
+ }
+
const returnValue = __('Are you sure you want to lose unsaved changes?');
if (!this.someUncommittedChanges) return undefined;
@@ -103,6 +114,9 @@ export default {
});
return returnValue;
},
+ handleSkipBeforeUnload() {
+ this.skipBeforeUnload = true;
+ },
openFile(file) {
this.$router.push(this.getUrlForPath(file.path));
},
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index 6f42ae48cc9..bf99538a2ad 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -13,6 +13,11 @@ export default {
required: false,
default: () => [],
},
+ initOpenView: {
+ type: String,
+ required: false,
+ default: '',
+ },
side: {
type: String,
required: true,
@@ -44,6 +49,9 @@ export default {
return this.tabViews.filter((view) => this.isAliveView(view.name));
},
},
+ created() {
+ this.openViewByName(this.initOpenView);
+ },
methods: {
...mapActions({
toggleOpen(dispatch) {
@@ -53,6 +61,13 @@ export default {
return dispatch(`${this.namespace}/open`, view);
},
}),
+ openViewByName(viewName) {
+ const view = viewName && this.tabViews.find((x) => x.name === viewName);
+
+ if (view) {
+ this.open(view);
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index da2d4fbe7f0..c74a5052573 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -7,6 +7,7 @@ import PipelinesList from '../pipelines/list.vue';
import Clientside from '../preview/clientside.vue';
import ResizablePanel from '../resizable_panel.vue';
import TerminalView from '../terminal/view.vue';
+import SwitchEditorsView from '../switch_editors/switch_editors_view.vue';
import CollapsibleSidebar from './collapsible_sidebar.vue';
// Need to add the width of the nav buttons since the resizable container contains those as well
@@ -20,7 +21,7 @@ export default {
},
computed: {
...mapState('terminal', { isTerminalVisible: 'isVisible' }),
- ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
+ ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled', 'canUseNewWebIde']),
...mapGetters(['packageJson']),
...mapState('rightPane', ['isOpen']),
showLivePreview() {
@@ -29,6 +30,12 @@ export default {
rightExtensionTabs() {
return [
{
+ show: this.canUseNewWebIde,
+ title: __('Switch editors'),
+ views: [{ component: SwitchEditorsView, ...rightSidebarViews.switchEditors }],
+ icon: 'bullhorn',
+ },
+ {
show: true,
title: __('Pipelines'),
views: [
@@ -53,6 +60,7 @@ export default {
},
},
WIDTH,
+ SWITCH_EDITORS_VIEW_NAME: rightSidebarViews.switchEditors.name,
};
</script>
@@ -64,6 +72,11 @@ export default {
:min-size="$options.WIDTH"
:resizable="isOpen"
>
- <collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" />
+ <collapsible-sidebar
+ class="gl-w-full"
+ :extension-tabs="rightExtensionTabs"
+ :init-open-view="$options.SWITCH_EDITORS_VIEW_NAME"
+ side="right"
+ />
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue b/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue
new file mode 100644
index 00000000000..00164f65e33
--- /dev/null
+++ b/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue
@@ -0,0 +1,103 @@
+<script>
+import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
+import { mapState } from 'vuex';
+import { createAlert } from '~/flash';
+import { logError } from '~/lib/logger';
+import axios from '~/lib/utils/axios_utils';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
+import { s__, __ } from '~/locale';
+import eventHub from '../../eventhub';
+
+export const MSG_DESCRIPTION = s__('WebIDE|You are invited to experience the new Web IDE.');
+export const MSG_BUTTON_TEXT = s__('WebIDE|Switch to new Web IDE');
+export const MSG_LEARN_MORE = __('Learn more');
+export const MSG_TITLE = s__('WebIDE|Ready for something new?');
+
+export const MSG_CONFIRM = s__(
+ 'WebIDE|Are you sure you want to switch editors? You will lose any unsaved changes.',
+);
+export const MSG_ERROR_ALERT = s__(
+ 'WebIDE|Something went wrong while updating the user preferences. Please see developer console for details.',
+);
+
+export default {
+ components: {
+ GlButton,
+ GlEmptyState,
+ GlLink,
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ computed: {
+ ...mapState(['switchEditorSvgPath', 'links', 'userPreferencesPath']),
+ },
+ methods: {
+ async submitSwitch() {
+ const confirmed = await confirmAction(MSG_CONFIRM, {
+ primaryBtnText: __('Switch editors'),
+ cancelBtnText: __('Cancel'),
+ });
+
+ if (!confirmed) {
+ return;
+ }
+
+ try {
+ await axios.put(this.userPreferencesPath, {
+ user: { use_legacy_web_ide: false },
+ });
+ } catch (e) {
+ // why: We do not want to translate console logs
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ logError('Error while updating user preferences', e);
+ createAlert({
+ message: MSG_ERROR_ALERT,
+ });
+ return;
+ }
+
+ eventHub.$emit('skip-beforeunload');
+ window.location.reload();
+ },
+ // what: ignoreWhilePending prevents double confirmation boxes
+ onSwitchClicked: ignoreWhilePending(async function onSwitchClicked() {
+ this.loading = true;
+
+ try {
+ await this.submitSwitch();
+ } finally {
+ this.loading = false;
+ }
+ }),
+ },
+ MSG_TITLE,
+ MSG_DESCRIPTION,
+ MSG_BUTTON_TEXT,
+ MSG_LEARN_MORE,
+};
+</script>
+
+<template>
+ <div class="gl-h-full gl-display-flex gl-flex-direction-column gl-justify-content-center">
+ <gl-empty-state :svg-path="switchEditorSvgPath" :svg-height="150" :title="$options.MSG_TITLE">
+ <template #description>
+ <span>{{ $options.MSG_DESCRIPTION }}</span>
+ <gl-link :href="links.newWebIDEHelpPagePath">{{ $options.MSG_LEARN_MORE }}</gl-link
+ >.
+ </template>
+ <template #actions>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="loading"
+ @click="onSwitchClicked"
+ >{{ $options.MSG_BUTTON_TEXT }}</gl-button
+ >
+ </template>
+ </gl-empty-state>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index bfe4c3ac271..c8e737fa6f5 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -61,6 +61,7 @@ export const leftSidebarViews = {
};
export const rightSidebarViews = {
+ switchEditors: { name: 'switch-editors', keepAlive: true },
pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: { name: 'jobs-detail', keepAlive: false },
mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 1a191f6f76f..dec282239d9 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -60,9 +60,11 @@ export const initLegacyWebIDE = (el, options = {}) => {
committedStateSvgPath: el.dataset.committedStateSvgPath,
pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
promotionSvgPath: el.dataset.promotionSvgPath,
+ switchEditorSvgPath: el.dataset.switchEditorSvgPath,
});
this.setLinks({
webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
+ newWebIDEHelpPagePath: el.dataset.newWebIdeHelpPagePath,
forkInfo: el.dataset.forkInfo ? JSON.parse(el.dataset.forkInfo) : null,
});
this.init({
@@ -72,6 +74,8 @@ export const initLegacyWebIDE = (el, options = {}) => {
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
previewMarkdownPath: el.dataset.previewMarkdownPath,
+ canUseNewWebIde: parseBoolean(el.dataset.canUseNewWebIde),
+ userPreferencesPath: el.dataset.userPreferencesPath,
});
},
beforeDestroy() {
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 48648796e66..d11fc388d5e 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -110,6 +110,7 @@ export default {
committedStateSvgPath,
pipelinesEmptyStateSvgPath,
promotionSvgPath,
+ switchEditorSvgPath,
},
) {
Object.assign(state, {
@@ -118,6 +119,7 @@ export default {
committedStateSvgPath,
pipelinesEmptyStateSvgPath,
promotionSvgPath,
+ switchEditorSvgPath,
});
},
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 526987c750a..70efda970bf 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -33,4 +33,6 @@ export default () => ({
environmentsGuidanceAlertDismissed: false,
environmentsGuidanceAlertDetected: false,
previewMarkdownPath: '',
+ userPreferencesPath: '',
+ canUseNewWebIde: false,
});
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 0cdd64b1b98..66dff77eef8 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -10,6 +10,7 @@ import {
GlSprintf,
GlTable,
GlFormCheckbox,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
@@ -60,7 +61,9 @@ export default {
PaginationBar,
HelpPopover,
},
-
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
sourceUrl: {
type: String,
@@ -118,14 +121,14 @@ export default {
},
{
key: 'webUrl',
- label: s__('BulkImport|From source group'),
+ label: s__('BulkImport|Source group'),
thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! import-jobs-from-col`,
// eslint-disable-next-line @gitlab/require-i18n-strings
tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
},
{
key: 'importTarget',
- label: s__('BulkImport|To new group'),
+ label: s__('BulkImport|New group'),
thClass: `${DEFAULT_TH_CLASSES} import-jobs-to-col`,
tdClass: DEFAULT_TD_CLASSES,
},
@@ -665,6 +668,16 @@ export default {
@change="hasAllAvailableGroupsSelected ? clearSelected() : selectAllRows()"
/>
</template>
+ <template #head(importTarget)="data">
+ <span data-test-id="new-path-col">
+ <span class="gl-mr-2">{{ data.label }}</span
+ ><gl-icon
+ v-gl-tooltip="s__('BulkImport|Path of the new group.')"
+ name="information"
+ :size="12"
+ />
+ </span>
+ </template>
<template #cell(selected)="{ rowSelected, selectRow, unselectRow, item: group }">
<gl-form-checkbox
class="gl-h-7 gl-pt-3"
diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
index a8fdf9b9ec5..cf1a4de68ed 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
@@ -40,6 +40,8 @@ export default {
v-for="{ name, label, details } in stages"
:key="name"
:checked="value[name]"
+ :data-qa-option-name="name"
+ data-qa-selector="advanced_settings_checkbox"
@change="$emit('input', { ...value, [name]: $event })"
>
{{ label }}
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 4daa9e8a1b8..df26d6ac4f6 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -39,7 +39,7 @@ export function initStoreFromElement(element) {
export function initPropsFromElement(element) {
return {
- providerTitle: element.dataset.provider,
+ providerTitle: element.dataset.providerTitle,
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
optionalStages: JSON.parse(element.dataset.optionalStages),
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 2806b785816..392dd63b089 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -85,10 +85,12 @@ export const billingPlanNames = {
};
const INTEGRATION_TYPE_SLACK = 'slack';
+const INTEGRATION_TYPE_SLACK_APPLICATION = 'gitlab_slack_application';
const INTEGRATION_TYPE_MATTERMOST = 'mattermost';
export const placeholderForType = {
[INTEGRATION_TYPE_SLACK]: __('#general, #development'),
+ [INTEGRATION_TYPE_SLACK_APPLICATION]: __('#general, #development'),
[INTEGRATION_TYPE_MATTERMOST]: __('my-channel'),
};
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 15f76c16516..4bf2b8d4468 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlBadge,
GlButton,
GlModalDirective,
@@ -9,7 +10,7 @@ import {
import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
-
+import { s__ } from '~/locale';
import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
@@ -59,6 +60,7 @@ export default {
import(
/* webpackChunkName: 'integrationSectionTrigger' */ '~/integrations/edit/components/sections/trigger.vue'
),
+ GlAlert,
GlBadge,
GlButton,
GlForm,
@@ -223,6 +225,12 @@ export default {
csrf,
integrationFormSectionComponents,
billingPlanNames,
+ slackUpgradeInfo: {
+ title: s__(
+ `SlackIntegration|Notifications only work if you're on the latest version of the GitLab for Slack app`,
+ ),
+ btnText: s__('SlackIntegration|Update to the latest version'),
+ },
};
</script>
@@ -277,6 +285,15 @@ export default {
</section>
<template v-if="hasSections">
+ <div v-if="customState.shouldUpgradeSlack && isSlackIntegration" class="gl-border-t">
+ <gl-alert
+ :title="$options.slackUpgradeInfo.title"
+ variant="warning"
+ :primary-button-link="customState.upgradeSlackUrl"
+ :primary-button-text="$options.slackUpgradeInfo.btnText"
+ class="gl-mb-8 gl-mt-5"
+ />
+ </div>
<div
v-for="(section, index) in customState.sections"
:key="section.type"
diff --git a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
index 403bad3db11..41cd650f932 100644
--- a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
@@ -7,18 +7,12 @@ export default {
components: {
GlModal,
},
- computed: {
- primaryProps() {
- return {
- text: __('Reset'),
- attributes: [{ variant: 'danger' }, { category: 'primary' }],
- };
- },
- cancelProps() {
- return {
- text: __('Cancel'),
- };
- },
+ primaryProps: {
+ text: __('Reset'),
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ },
+ cancelProps: {
+ text: __('Cancel'),
},
methods: {
onReset() {
@@ -33,8 +27,8 @@ export default {
modal-id="confirmResetIntegration"
size="sm"
:title="s__('Integrations|Reset integration?')"
- :action-primary="primaryProps"
- :action-cancel="cancelProps"
+ :action-primary="$options.primaryProps"
+ :action-cancel="$options.cancelProps"
@primary="onReset"
>
<p>
diff --git a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
index 67647cadf19..3820a87e5ad 100644
--- a/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/trigger_fields.vue
@@ -23,7 +23,7 @@ export default {
},
computed: {
...mapGetters(['isInheriting']),
- placeholder() {
+ defaultPlaceholder() {
return placeholderForType[this.type];
},
},
@@ -55,7 +55,7 @@ export default {
v-if="event.field"
v-model="event.field.value"
:name="fieldName(event.field.name)"
- :placeholder="placeholder"
+ :placeholder="event.field.placeholder || defaultPlaceholder"
:readonly="isInheriting"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 2360588ab39..f15ad5e052e 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -36,6 +36,7 @@ function parseDatasetToProps(data) {
jiraIssueTransitionAutomatic,
jiraIssueTransitionId,
redirectTo,
+ upgradeSlackUrl,
...booleanAttributes
} = data;
const {
@@ -51,6 +52,7 @@ function parseDatasetToProps(data) {
showJiraVulnerabilitiesIntegration,
enableJiraIssues,
enableJiraVulnerabilities,
+ shouldUpgradeSlack,
} = parseBooleanInData(booleanAttributes);
return {
@@ -89,6 +91,8 @@ function parseDatasetToProps(data) {
integrationLevel,
id: parseInt(id, 10),
redirectTo,
+ shouldUpgradeSlack,
+ upgradeSlackUrl,
};
}
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 a334f5e4bf7..f61e822bf7e 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -18,7 +18,8 @@ import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { getParameterValues } from '~/lib/utils/url_utility';
import { n__, sprintf } from '~/locale';
import {
- CLOSE_TO_LIMIT_COUNT,
+ CLOSE_TO_LIMIT_VARIANT,
+ REACHED_LIMIT_VARIANT,
USERS_FILTER_ALL,
INVITE_MEMBERS_FOR_TASK,
MEMBER_MODAL_LABELS,
@@ -174,27 +175,11 @@ export default {
isOnLearnGitlab() {
return this.source === LEARN_GITLAB;
},
- closeToLimit() {
- if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
- return (
- this.usersLimitDataset.membersCount >=
- this.usersLimitDataset.freeUsersLimit - CLOSE_TO_LIMIT_COUNT
- );
- }
-
- return false;
- },
- reachedLimit() {
- if (this.usersLimitDataset.freeUsersLimit && this.usersLimitDataset.membersCount) {
- return this.usersLimitDataset.membersCount >= this.usersLimitDataset.freeUsersLimit;
- }
-
- return false;
+ showUserLimitNotification() {
+ return this.usersLimitDataset.reachedLimit || this.usersLimitDataset.closeToDashboardLimit;
},
- formGroupDescription() {
- return this.reachedLimit
- ? this.$options.labels.placeHolderDisabled
- : this.$options.labels.placeHolder;
+ limitVariant() {
+ return this.usersLimitDataset.reachedLimit ? REACHED_LIMIT_VARIANT : CLOSE_TO_LIMIT_VARIANT;
},
errorList() {
return Object.entries(this.invalidMembers).map(([member, error]) => {
@@ -385,13 +370,11 @@ export default {
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="$options.labels.searchField"
- :form-group-description="formGroupDescription"
+ :form-group-description="$options.labels.placeHolder"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
- :close-to-limit="closeToLimit"
- :reached-limit="reachedLimit"
:users-limit-dataset="usersLimitDataset"
@reset="resetFields"
@submit="sendInvite"
@@ -448,9 +431,8 @@ export default {
</template>
</gl-alert>
<user-limit-notification
- v-else
- :close-to-limit="closeToLimit"
- :reached-limit="reachedLimit"
+ v-else-if="showUserLimitNotification"
+ :limit-variant="limitVariant"
:users-limit-dataset="usersLimitDataset"
/>
</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 f917ebc35c2..e3511a49fc5 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -8,7 +8,6 @@ import {
GlLink,
GlSprintf,
GlFormInput,
- GlIcon,
} from '@gitlab/ui';
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
@@ -20,11 +19,8 @@ import {
INVITE_BUTTON_TEXT,
INVITE_BUTTON_TEXT_DISABLED,
CANCEL_BUTTON_TEXT,
- CANCEL_BUTTON_TEXT_DISABLED,
HEADER_CLOSE_LABEL,
ON_SHOW_TRACK_LABEL,
- ON_CLOSE_TRACK_LABEL,
- ON_SUBMIT_TRACK_LABEL,
} from '../constants';
const DEFAULT_SLOT = 'default';
@@ -48,7 +44,6 @@ export default {
GlDropdownItem,
GlSprintf,
GlFormInput,
- GlIcon,
ContentTransition,
},
mixins: [Tracking.mixin()],
@@ -131,16 +126,6 @@ export default {
required: false,
default: false,
},
- closeToLimit: {
- type: Boolean,
- required: false,
- default: false,
- },
- reachedLimit: {
- type: Boolean,
- required: false,
- default: false,
- },
usersLimitDataset: {
type: Object,
required: false,
@@ -175,20 +160,17 @@ export default {
},
actionPrimary() {
return {
- text: this.reachedLimit ? INVITE_BUTTON_TEXT_DISABLED : this.submitButtonText,
+ text: this.submitButtonText,
attributes: {
variant: 'confirm',
- disabled: this.reachedLimit ? false : this.submitDisabled,
- loading: this.reachedLimit ? false : this.isLoading,
+ disabled: this.submitDisabled,
+ loading: this.isLoading,
'data-qa-selector': 'invite_button',
- ...(this.reachedLimit && { href: this.usersLimitDataset.membersPath }),
},
};
},
actionCancel() {
- if (this.reachedLimit && this.usersLimitDataset.userNamespace) return undefined;
-
- if (this.closeToLimit && this.usersLimitDataset.userNamespace) {
+ if (this.usersLimitDataset.closeToDashboardLimit && this.usersLimitDataset.userNamespace) {
return {
text: INVITE_BUTTON_TEXT_DISABLED,
attributes: {
@@ -200,13 +182,9 @@ export default {
}
return {
- text: this.reachedLimit ? CANCEL_BUTTON_TEXT_DISABLED : this.cancelButtonText,
- ...(this.reachedLimit && { attributes: { href: this.usersLimitDataset.purchasePath } }),
+ text: this.cancelButtonText,
};
},
- selectLabelClass() {
- return `col-form-label ${this.reachedLimit ? 'gl-text-gray-500' : ''}`;
- },
},
watch: {
selectedAccessLevel: {
@@ -226,23 +204,19 @@ export default {
this.$emit('reset');
},
onShowModal() {
- if (this.reachedLimit) {
+ if (this.usersLimitDataset.reachedLimit) {
this.track('render', { category: 'default', label: ON_SHOW_TRACK_LABEL });
}
},
onCloseModal(e) {
- if (this.preventCancelDefault || this.reachedLimit) {
+ if (this.preventCancelDefault) {
e.preventDefault();
} else {
this.onReset();
this.$refs.modal.hide();
}
- if (this.reachedLimit) {
- this.track('click_button', { category: 'default', label: ON_CLOSE_TRACK_LABEL });
- } else {
- this.$emit('cancel');
- }
+ this.$emit('cancel');
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
@@ -251,14 +225,10 @@ export default {
// We never want to hide when submitting
e.preventDefault();
- if (this.reachedLimit) {
- this.track('click_button', { category: 'default', label: ON_SUBMIT_TRACK_LABEL });
- } else {
- this.$emit('submit', {
- accessLevel: this.selectedAccessLevel,
- expiresAt: this.selectedDate,
- });
- }
+ this.$emit('submit', {
+ accessLevel: this.selectedAccessLevel,
+ expiresAt: this.selectedDate,
+ });
},
},
HEADER_CLOSE_LABEL,
@@ -311,71 +281,63 @@ export default {
<gl-form-group
:invalid-feedback="invalidFeedbackMessage"
:state="exceptionState"
+ :description="formGroupDescription"
data-testid="members-form-group"
>
- <template #description>
- <gl-icon v-if="reachedLimit" name="lock" />
- {{ formGroupDescription }}
- </template>
-
- <label :id="selectLabelId" :class="selectLabelClass">{{ labelSearchField }}</label>
- <gl-form-input v-if="reachedLimit" data-testid="disabled-input" disabled />
- <slot v-else name="select" v-bind="{ exceptionState, labelId: selectLabelId }"></slot>
+ <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
+ <slot name="select" v-bind="{ exceptionState, labelId: selectLabelId }"></slot>
</gl-form-group>
- <template v-if="!reachedLimit">
- <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
-
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-dropdown
- class="gl-shadow-none gl-w-full"
- data-qa-selector="access_level_dropdown"
- v-bind="$attrs"
- :text="selectedRoleName"
- >
- <template v-for="(key, item) in accessLevels">
- <gl-dropdown-item
- :key="key"
- active-class="is-active"
- is-check-item
- :is-checked="key === selectedAccessLevel"
- @click="changeSelectedItem(key)"
- >
- <div>{{ item }}</div>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
- </div>
+ <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
+ <div class="gl-mt-2 gl-w-half gl-xs-w-full">
+ <gl-dropdown
+ class="gl-shadow-none gl-w-full"
+ data-qa-selector="access_level_dropdown"
+ v-bind="$attrs"
+ :text="selectedRoleName"
+ >
+ <template v-for="(key, item) in accessLevels">
+ <gl-dropdown-item
+ :key="key"
+ active-class="is-active"
+ is-check-item
+ :is-checked="key === selectedAccessLevel"
+ @click="changeSelectedItem(key)"
+ >
+ <div>{{ item }}</div>
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+ </div>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-sprintf :message="$options.READ_MORE_TEXT">
- <template #link="{ content }">
- <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
+ <div class="gl-mt-2 gl-w-half gl-xs-w-full">
+ <gl-sprintf :message="$options.READ_MORE_TEXT">
+ <template #link="{ content }">
+ <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
- <label class="gl-mt-5 gl-display-block" for="expires_at">{{
- $options.ACCESS_EXPIRE_DATE
- }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
- <gl-datepicker
- v-model="selectedDate"
- class="gl-display-inline!"
- :min-date="minDate"
- :target="null"
- >
- <template #default="{ formattedDate }">
- <gl-form-input
- class="gl-w-full"
- :value="formattedDate"
- :placeholder="__(`YYYY-MM-DD`)"
- />
- </template>
- </gl-datepicker>
- </div>
- <slot name="form-after"></slot>
- </template>
+ <label class="gl-mt-5 gl-display-block" for="expires_at">{{
+ $options.ACCESS_EXPIRE_DATE
+ }}</label>
+ <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
+ <gl-datepicker
+ v-model="selectedDate"
+ class="gl-display-inline!"
+ :min-date="minDate"
+ :target="null"
+ >
+ <template #default="{ formattedDate }">
+ <gl-form-input
+ class="gl-w-full"
+ :value="formattedDate"
+ :placeholder="__(`YYYY-MM-DD`)"
+ />
+ </template>
+ </gl-datepicker>
+ </div>
+ <slot name="form-after"></slot>
</template>
<template v-for="{ key } in extraSlots" #[key]>
diff --git a/app/assets/javascripts/invite_members/components/user_limit_notification.vue b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
index c3d9d959ef6..515dd3de319 100644
--- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue
+++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
@@ -5,9 +5,10 @@ import { n__, sprintf } from '~/locale';
import {
WARNING_ALERT_TITLE,
DANGER_ALERT_TITLE,
- REACHED_LIMIT_MESSAGE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
+ REACHED_LIMIT_VARIANT,
CLOSE_TO_LIMIT_MESSAGE,
+ CLOSE_TO_LIMIT_VARIANT,
} from '../constants';
export default {
@@ -15,87 +16,71 @@ export default {
components: { GlAlert, GlSprintf, GlLink },
inject: ['name'],
props: {
- closeToLimit: {
- type: Boolean,
- required: true,
- },
- reachedLimit: {
- type: Boolean,
+ limitVariant: {
+ type: String,
required: true,
},
usersLimitDataset: {
type: Object,
- required: false,
- default: () => ({}),
+ required: true,
},
},
computed: {
- freeUsersLimit() {
- return this.usersLimitDataset.freeUsersLimit;
- },
- membersCount() {
- return this.usersLimitDataset.membersCount;
- },
- newTrialRegistrationPath() {
- return this.usersLimitDataset.newTrialRegistrationPath;
- },
- purchasePath() {
- return this.usersLimitDataset.purchasePath;
- },
- warningAlertTitle() {
- return sprintf(WARNING_ALERT_TITLE, {
- count: this.freeUsersLimit - this.membersCount,
- members: this.pluralMembers(this.freeUsersLimit - this.membersCount),
- name: this.name,
- });
- },
- dangerAlertTitle() {
- return sprintf(DANGER_ALERT_TITLE, {
- count: this.freeUsersLimit,
- members: this.pluralMembers(this.freeUsersLimit),
- name: this.name,
- });
- },
- variant() {
- return this.reachedLimit ? 'danger' : 'warning';
- },
- title() {
- return this.reachedLimit ? this.dangerAlertTitle : this.warningAlertTitle;
- },
- message() {
- if (this.reachedLimit) {
- return this.$options.i18n.reachedLimitUpgradeSuggestionMessage;
- }
-
- return this.$options.i18n.closeToLimitMessage;
+ limitAttributes() {
+ return {
+ [CLOSE_TO_LIMIT_VARIANT]: {
+ variant: 'warning',
+ title: this.title(WARNING_ALERT_TITLE, this.usersLimitDataset.remainingSeats),
+ message: CLOSE_TO_LIMIT_MESSAGE,
+ },
+ [REACHED_LIMIT_VARIANT]: {
+ variant: 'danger',
+ title: this.title(DANGER_ALERT_TITLE, this.usersLimitDataset.freeUsersLimit),
+ message: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
+ },
+ };
},
},
methods: {
- pluralMembers(count) {
- return n__('member', 'members', count);
+ title(titleTemplate, count) {
+ return sprintf(titleTemplate, {
+ count,
+ members: n__('member', 'members', count),
+ name: this.name,
+ });
},
},
- i18n: {
- reachedLimitMessage: REACHED_LIMIT_MESSAGE,
- reachedLimitUpgradeSuggestionMessage: REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
- closeToLimitMessage: CLOSE_TO_LIMIT_MESSAGE,
- },
};
</script>
<template>
<gl-alert
- v-if="reachedLimit || closeToLimit"
- :variant="variant"
+ :variant="limitAttributes[limitVariant].variant"
:dismissible="false"
- :title="title"
+ :title="limitAttributes[limitVariant].title"
>
- <gl-sprintf :message="message">
+ <gl-sprintf :message="limitAttributes[limitVariant].message">
<template #trialLink="{ content }">
- <gl-link :href="newTrialRegistrationPath" class="gl-label-link">{{ content }}</gl-link>
+ <gl-link
+ :href="usersLimitDataset.newTrialRegistrationPath"
+ class="gl-label-link"
+ data-track-action="click_link"
+ :data-track-label="`start_trial_user_limit_notification_${limitVariant}`"
+ data-testid="trial-link"
+ >
+ {{ content }}
+ </gl-link>
</template>
<template #upgradeLink="{ content }">
- <gl-link :href="purchasePath" class="gl-label-link">{{ content }}</gl-link>
+ <gl-link
+ :href="usersLimitDataset.purchasePath"
+ class="gl-label-link"
+ data-track-action="click_link"
+ :data-track-label="`upgrade_user_limit_notification_${limitVariant}`"
+ data-testid="upgrade-link"
+ >
+ {{ content }}
+ </gl-link>
</template>
</gl-sprintf>
</gl-alert>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index f502e1ea369..de7b1019782 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,6 +1,5 @@
import { s__ } from '~/locale';
-export const CLOSE_TO_LIMIT_COUNT = 2;
export const SEARCH_DELAY = 200;
export const VALID_TOKEN_BACKGROUND = 'gl-bg-green-100';
export const INVALID_TOKEN_BACKGROUND = 'gl-bg-red-100';
@@ -40,9 +39,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_PLACEHOLDER_DISABLED = s__(
- 'InviteMembersModal|This feature is disabled until this group has space for more members.',
-);
export const MEMBERS_TASKS_TO_BE_DONE_TITLE = s__(
'InviteMembersModal|Create issues for your new team member to work on (optional)',
);
@@ -110,7 +106,6 @@ export const MEMBER_MODAL_LABELS = {
},
searchField: MEMBERS_SEARCH_FIELD,
placeHolder: MEMBERS_PLACEHOLDER,
- placeHolderDisabled: MEMBERS_PLACEHOLDER_DISABLED,
tasksToBeDone: {
title: MEMBERS_TASKS_TO_BE_DONE_TITLE,
noProjects: MEMBERS_TASKS_TO_BE_DONE_NO_PROJECTS,
@@ -139,9 +134,7 @@ export const GROUP_MODAL_LABELS = {
};
export const LEARN_GITLAB = 'learn_gitlab';
-export const ON_SHOW_TRACK_LABEL = 'locked_modal_viewed';
-export const ON_CLOSE_TRACK_LABEL = 'explore_paid_plans_clicked';
-export const ON_SUBMIT_TRACK_LABEL = 'manage_members_clicked';
+export const ON_SHOW_TRACK_LABEL = 'over_limit_modal_viewed';
export const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
@@ -150,13 +143,16 @@ export const DANGER_ALERT_TITLE = s__(
"InviteMembersModal|You've reached your %{count} %{members} limit for %{name}",
);
+export const REACHED_LIMIT_VARIANT = 'reached';
+export const CLOSE_TO_LIMIT_VARIANT = 'close';
+
export const REACHED_LIMIT_MESSAGE = s__(
- 'InviteMembersModal|You cannot add more members, but you can remove members who no longer need access.',
+ 'InviteMembersModal|To invite new users to this namespace, you must remove existing users. You can still add existing namespace users.',
);
export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.concat(
s__(
- 'InviteMembersModal| To get more members and access to additional paid features, an owner of the group can start a trial or upgrade to a paid tier.',
+ 'InviteMembersModal| To get more members, the owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
),
);
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql
new file mode 100644
index 00000000000..d350072425b
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql
@@ -0,0 +1,5 @@
+mutation moveIssue($moveIssueInput: IssueMoveInput!) {
+ issueMove(input: $moveIssueInput) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue
new file mode 100644
index 00000000000..6e287ac3bb7
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue
@@ -0,0 +1,171 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
+import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
+import { s__ } from '~/locale';
+import {
+ WORK_ITEM_TYPE_ENUM_ISSUE,
+ WORK_ITEM_TYPE_ENUM_INCIDENT,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_ENUM_TEST_CASE,
+} from '~/work_items/constants';
+import issuableEventHub from '~/issues/list/eventhub';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import moveIssueMutation from './graphql/mutations/move_issue.mutation.graphql';
+
+export default {
+ name: 'MoveIssuesButton',
+ components: {
+ IssuableMoveDropdown,
+ GlAlert,
+ },
+ props: {
+ projectFullPath: {
+ type: String,
+ required: true,
+ },
+ projectsFetchPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selectedIssuables: [],
+ moveInProgress: false,
+ };
+ },
+ computed: {
+ cannotMoveTasksWarningTitle() {
+ if (this.tasksSelected && this.testCasesSelected) {
+ return s__('Issues|Tasks and test cases can not be moved.');
+ }
+
+ if (this.testCasesSelected) {
+ return s__('Issues|Test cases can not be moved.');
+ }
+
+ return s__('Issues|Tasks can not be moved.');
+ },
+ issuesSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_ISSUE);
+ },
+ incidentsSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_INCIDENT);
+ },
+ tasksSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TASK);
+ },
+ testCasesSelected() {
+ return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TEST_CASE);
+ },
+ },
+ mounted() {
+ issuableEventHub.$on('issuables:issuableChecked', this.handleIssuableChecked);
+ },
+ beforeDestroy() {
+ issuableEventHub.$off('issuables:issuableChecked', this.handleIssuableChecked);
+ },
+ methods: {
+ handleIssuableChecked(issuable, value) {
+ if (value) {
+ this.selectedIssuables.push(issuable);
+ } else {
+ const index = this.selectedIssuables.indexOf(issuable);
+ if (index > -1) {
+ this.selectedIssuables.splice(index, 1);
+ }
+ }
+ },
+ moveIssues(targetProject) {
+ const iids = this.selectedIssuables.reduce((result, issueData) => {
+ if (
+ issueData.type === WORK_ITEM_TYPE_ENUM_ISSUE ||
+ issueData.type === WORK_ITEM_TYPE_ENUM_INCIDENT
+ ) {
+ result.push(issueData.iid);
+ }
+ return result;
+ }, []);
+
+ if (iids.length === 0) {
+ return;
+ }
+
+ this.moveInProgress = true;
+ issuableEventHub.$emit('issuables:bulkMoveStarted');
+
+ const promises = iids.map((id) => {
+ return this.moveIssue(id, targetProject);
+ });
+
+ Promise.all(promises)
+ .then((promisesResult) => {
+ let foundError = false;
+
+ for (const promiseResult of promisesResult) {
+ if (promiseResult.data.issueMove?.errors?.length) {
+ foundError = true;
+ logError(
+ `Error moving issue. Error message: ${promiseResult.data.issueMove.errors[0].message}`,
+ );
+ }
+ }
+
+ if (!foundError) {
+ const client = this.$apollo.provider.defaultClient;
+ client.refetchQueries({
+ include: [getIssuesQuery, getIssuesCountQuery],
+ });
+ this.moveInProgress = false;
+ this.selectedIssuables = [];
+ issuableEventHub.$emit('issuables:bulkMoveEnded');
+ } else {
+ throw new Error();
+ }
+ })
+ .catch(() => {
+ this.moveInProgress = false;
+ issuableEventHub.$emit('issuables:bulkMoveEnded');
+
+ createFlash({
+ message: s__(`Issues|There was an error while moving the issues.`),
+ });
+ });
+ },
+ moveIssue(issueIid, targetProject) {
+ return this.$apollo.mutate({
+ mutation: moveIssueMutation,
+ variables: {
+ moveIssueInput: {
+ projectPath: this.projectFullPath,
+ iid: issueIid,
+ targetProjectPath: targetProject.full_path,
+ },
+ },
+ });
+ },
+ },
+ i18n: {
+ dropdownButtonTitle: s__('Issues|Move selected'),
+ },
+};
+</script>
+<template>
+ <div>
+ <issuable-move-dropdown
+ :project-full-path="projectFullPath"
+ :projects-fetch-path="projectsFetchPath"
+ :move-in-progress="moveInProgress"
+ :disabled="!issuesSelected && !incidentsSelected"
+ :dropdown-header-title="$options.i18n.dropdownButtonTitle"
+ :dropdown-button-title="$options.i18n.dropdownButtonTitle"
+ @move-issuable="moveIssues"
+ />
+ <gl-alert v-if="tasksSelected || testCasesSelected" :dismissible="false" variant="warning">
+ {{ cannotMoveTasksWarningTitle }}
+ </gl-alert>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
index 4657771353f..b7cb805ee37 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { gqlClient } from '../../issues/list/graphql';
import StatusDropdown from './components/status_dropdown.vue';
import SubscriptionsDropdown from './components/subscriptions_dropdown.vue';
+import MoveIssuesButton from './components/move_issues_button.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
@@ -42,3 +45,31 @@ export function initSubscriptionsDropdown() {
render: (createElement) => createElement(SubscriptionsDropdown),
});
}
+
+export function initMoveIssuesButton() {
+ const el = document.querySelector('.js-move-issues');
+
+ if (!el) {
+ return null;
+ }
+
+ const { dataset } = el;
+
+ Vue.use(VueApollo);
+ const apolloProvider = new VueApollo({
+ defaultClient: gqlClient,
+ });
+
+ return new Vue({
+ el,
+ name: 'MoveIssuesRoot',
+ apolloProvider,
+ render: (createElement) =>
+ createElement(MoveIssuesButton, {
+ props: {
+ projectFullPath: dataset.projectFullPath,
+ projectsFetchPath: dataset.projectsFetchPath,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
index a33c6ae8030..b46a95c7dfa 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
-import MilestoneSelect from '~/milestones/milestone_select';
+import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
const HIDDEN_CLASS = 'hidden';
@@ -46,35 +46,28 @@ export default class IssuableBulkUpdateSidebar {
// https://gitlab.com/gitlab-org/gitlab/-/issues/325874
issuableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true));
issuableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState());
+
+ // These events are connected to the logic inside `move_issues_button.vue`,
+ // so that only one action can be performed at a time
+ issuableEventHub.$on('issuables:bulkMoveStarted', () => this.toggleSubmitButtonDisabled(true));
+ issuableEventHub.$on('issuables:bulkMoveEnded', () => this.updateFormState());
}
initDropdowns() {
new LabelsSelect();
- new MilestoneSelect();
+ mountMilestoneDropdown();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at
// runtime this block won't execute.
if (IS_EE) {
- import('ee_else_ce/vue_shared/components/sidebar/health_status_select/health_status_bundle')
- .then(({ default: HealthStatusSelect }) => {
- HealthStatusSelect();
- })
- .catch(() => {});
-
- import('ee_else_ce/vue_shared/components/sidebar/epics_select/epics_select_bundle')
- .then(({ default: EpicSelect }) => {
- EpicSelect();
+ import('ee_else_ce/sidebar/mount_sidebar')
+ .then(({ mountEpicDropdown, mountHealthStatusDropdown, mountIterationDropdown }) => {
+ mountEpicDropdown();
+ mountHealthStatusDropdown();
+ mountIterationDropdown();
})
.catch(() => {});
-
- import('ee_else_ce/vue_shared/components/sidebar/iterations_dropdown_bundle')
- .then(({ default: iterationsDropdown }) => {
- iterationsDropdown();
- })
- .catch((e) => {
- throw e;
- });
}
}
@@ -89,6 +82,8 @@ export default class IssuableBulkUpdateSidebar {
this.updateSelectedIssuableIds();
IssuableBulkUpdateActions.setOriginalDropdownData();
+
+ issuableEventHub.$emit('issuables:selectionChanged', !noCheckedIssues);
}
prepForSubmit() {
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 8894e8f63b8..254248ef1d4 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -141,6 +141,7 @@ export default {
<gl-link
:href="computedPath"
class="sortable-link gl-font-weight-normal"
+ target="_blank"
@click="handleTitleClick"
>
{{ title }}
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
new file mode 100644
index 00000000000..29f6aecca03
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { __ } from '~/locale';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+
+export default {
+ i18n: {
+ calendarButtonText: __('Subscribe to calendar'),
+ emptyStateTitle: __('Please select at least one filter to see results'),
+ rssButtonText: __('Subscribe to RSS feed'),
+ searchInputPlaceholder: __('Search or filter results...'),
+ },
+ IssuableListTabs,
+ components: {
+ GlButton,
+ GlEmptyState,
+ IssuableList,
+ },
+ inject: ['calendarPath', 'emptyStateSvgPath', 'isSignedIn', 'rssPath'],
+ data() {
+ return {
+ issues: [],
+ searchTokens: [],
+ sortOptions: [],
+ state: IssuableStates.Opened,
+ };
+ },
+};
+</script>
+
+<template>
+ <issuable-list
+ namespace="dashboard"
+ recent-searches-storage-key="issues"
+ :search-input-placeholder="$options.i18n.searchInputPlaceholder"
+ :search-tokens="searchTokens"
+ :sort-options="sortOptions"
+ :issuables="issues"
+ :tabs="$options.IssuableListTabs"
+ :current-tab="state"
+ >
+ <template #nav-actions>
+ <gl-button :href="rssPath" icon="rss">
+ {{ $options.i18n.rssButtonText }}
+ </gl-button>
+ <gl-button :href="calendarPath" icon="calendar">
+ {{ $options.i18n.calendarButtonText }}
+ </gl-button>
+ </template>
+
+ <template #empty-state>
+ <gl-empty-state :svg-path="emptyStateSvgPath" :title="$options.i18n.emptyStateTitle" />
+ </template>
+ </issuable-list>
+</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
new file mode 100644
index 00000000000..a1ae3b93f7d
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import IssuesDashboardApp from './components/issues_dashboard_app.vue';
+
+export function mountIssuesDashboardApp() {
+ const el = document.querySelector('.js-issues-dashboard');
+
+ if (!el) {
+ return null;
+ }
+
+ const { calendarPath, emptyStateSvgPath, isSignedIn, rssPath } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'IssuesDashboardRoot',
+ provide: {
+ calendarPath,
+ emptyStateSvgPath,
+ isSignedIn: parseBoolean(isSignedIn),
+ rssPath,
+ },
+ render: (createComponent) => createComponent(IssuesDashboardApp),
+ });
+}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index 22ac37656ea..a785790169d 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -18,9 +18,9 @@ import {
} from '~/issues/show';
import { parseIssuableData } from '~/issues/show/utils/parse_data';
import LabelsSelect from '~/labels/labels_select';
-import MilestoneSelect from '~/milestones/milestone_select';
import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
+import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
import ZenMode from '~/zen_mode';
import initAwardsApp from '~/emoji/awards_app';
import initLinkedResources from '~/linked_resources';
@@ -41,11 +41,11 @@ export function initForm() {
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new
new LabelsSelect(); // eslint-disable-line no-new
- new MilestoneSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initTitleSuggestions();
initTypePopover();
+ mountMilestoneDropdown();
}
export function initShow() {
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 acb6aa93f0f..64de4b1947b 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -27,6 +27,7 @@ import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
DEFAULT_NONE_ANY,
+ FILTERED_SEARCH_TERM,
OPERATOR_IS_ONLY,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
@@ -38,12 +39,22 @@ import {
TOKEN_TITLE_ORGANIZATION,
TOKEN_TITLE_RELEASE,
TOKEN_TITLE_TYPE,
- FILTERED_SEARCH_TERM,
+ OPERATOR_IS_NOT_OR,
+ OPERATOR_IS_AND_IS_NOT,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { WORK_ITEM_TYPE_ENUM_TASK } from '~/work_items/constants';
import {
CREATED_DESC,
defaultTypeTokenOptions,
@@ -59,16 +70,6 @@ import {
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_ASC,
- TOKEN_TYPE_ASSIGNEE,
- TOKEN_TYPE_AUTHOR,
- TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_CONTACT,
- TOKEN_TYPE_LABEL,
- TOKEN_TYPE_MILESTONE,
- TOKEN_TYPE_MY_REACTION,
- TOKEN_TYPE_ORGANIZATION,
- TOKEN_TYPE_RELEASE,
- TOKEN_TYPE_TYPE,
TYPE_TOKEN_TASK_OPTION,
UPDATED_DESC,
urlSortParams,
@@ -140,7 +141,6 @@ export default {
'hasAnyProjects',
'hasBlockedIssuesFeature',
'hasIssueWeightsFeature',
- 'hasMultipleIssueAssigneesFeature',
'hasScopedLabelsFeature',
'initialEmail',
'initialSort',
@@ -239,21 +239,17 @@ export default {
state: this.state,
...this.pageParams,
...this.apiFilterParams,
- types: this.apiFilterParams.types || this.defaultWorkItemTypes,
+ types: this.apiFilterParams.types || defaultWorkItemTypes,
};
},
namespace() {
return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
},
- defaultWorkItemTypes() {
- return this.isWorkItemsEnabled
- ? defaultWorkItemTypes
- : defaultWorkItemTypes.filter((type) => type !== WORK_ITEM_TYPE_ENUM_TASK);
- },
typeTokenOptions() {
- return this.isWorkItemsEnabled
- ? defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION)
- : defaultTypeTokenOptions;
+ return defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION);
+ },
+ hasOrFeature() {
+ return this.glFeatures.orIssuableQueries;
},
hasSearch() {
return (
@@ -272,9 +268,6 @@ export default {
isOpenTab() {
return this.state === IssuableStates.Opened;
},
- isWorkItemsEnabled() {
- return this.glFeatures.workItems;
- },
showCsvButtons() {
return this.isProject && this.isSignedIn;
},
@@ -324,8 +317,8 @@ export default {
icon: 'user',
token: AuthorToken,
dataType: 'user',
- unique: !this.hasMultipleIssueAssigneesFeature,
defaultAuthors: DEFAULT_NONE_ANY,
+ operators: this.hasOrFeature ? OPERATOR_IS_NOT_OR : OPERATOR_IS_AND_IS_NOT,
fetchAuthors: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedAuthors,
@@ -565,6 +558,7 @@ export default {
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
bulkUpdateSidebar.initStatusDropdown();
bulkUpdateSidebar.initSubscriptionsDropdown();
+ bulkUpdateSidebar.initMoveIssuesButton();
const usersSelect = await import('~/users_select');
const UsersSelect = usersSelect.default;
@@ -793,6 +787,7 @@ export default {
:show-page-size-change-controls="showPageSizeControls"
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
+ :show-filtered-search-friendly-text="hasOrFeature"
show-work-item-type-icon
@click-tab="handleClickTab"
@dismiss-alert="handleDismissAlert"
@@ -959,12 +954,17 @@ export default {
<gl-empty-state
v-else
- :description="$options.i18n.noIssuesSignedOutDescription"
:title="$options.i18n.noIssuesSignedOutTitle"
:svg-path="emptyStateSvgPath"
:primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
:primary-button-link="signInPath"
- />
+ >
+ <template #description>
+ <gl-link :href="issuesHelpPagePath" target="_blank">{{
+ $options.i18n.noIssuesSignedOutDescription
+ }}</gl-link>
+ </template>
+ </gl-empty-state>
<issuable-by-email v-if="showIssuableByEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
</div>
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 9fe8899ab39..5ed9ceea856 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -7,7 +7,21 @@ import {
FILTER_UPCOMING,
OPERATOR_IS,
OPERATOR_IS_NOT,
+ OPERATOR_OR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_CONTACT,
+ TOKEN_TYPE_EPIC,
TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_ORGANIZATION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_WEIGHT,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
@@ -46,10 +60,8 @@ export const i18n = {
noIssuesSignedInDescription: __('Learn more about issues.'),
noIssuesSignedInTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noIssuesSignedOutButtonText: __('Register / Sign In'),
- noIssuesSignedOutDescription: __(
- 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
- ),
- noIssuesSignedOutTitle: __('There are no issues to show'),
+ noIssuesSignedOutDescription: __('Learn more about issues.'),
+ noIssuesSignedOutTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noSearchResultsDescription: __('To widen your search, change or remove filters above'),
noSearchResultsTitle: __('Sorry, your filter produced no results'),
relatedMergeRequests: __('Related merge requests'),
@@ -136,20 +148,6 @@ export const specialFilterValues = [
FILTER_STARTED,
];
-export const TOKEN_TYPE_AUTHOR = 'author_username';
-export const TOKEN_TYPE_ASSIGNEE = 'assignee_username';
-export const TOKEN_TYPE_MILESTONE = 'milestone';
-export const TOKEN_TYPE_LABEL = 'labels';
-export const TOKEN_TYPE_TYPE = 'type';
-export const TOKEN_TYPE_RELEASE = 'release';
-export const TOKEN_TYPE_MY_REACTION = 'my_reaction_emoji';
-export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
-export const TOKEN_TYPE_ITERATION = 'iteration';
-export const TOKEN_TYPE_EPIC = 'epic_id';
-export const TOKEN_TYPE_WEIGHT = 'weight';
-export const TOKEN_TYPE_CONTACT = 'crm_contact';
-export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
-
export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
// This should be consistent with Issue::TYPES_FOR_LIST in the backend
@@ -195,6 +193,9 @@ export const filters = {
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
+ [OPERATOR_OR]: {
+ [NORMAL_FILTER]: 'or[assignee_username][]',
+ },
},
},
[TOKEN_TYPE_MILESTONE]: {
diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js
new file mode 100644
index 00000000000..5ef61727a3d
--- /dev/null
+++ b/app/assets/javascripts/issues/list/graphql.js
@@ -0,0 +1,25 @@
+import produce from 'immer';
+import createDefaultClient from '~/lib/graphql';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+
+const resolvers = {
+ Mutation: {
+ reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
+ const variables = JSON.parse(serializedVariables);
+ const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
+
+ const data = produce(sourceData, (draftData) => {
+ const issues = draftData[namespace].issues.nodes.slice();
+ const issueToMove = issues[oldIndex];
+ issues.splice(oldIndex, 1);
+ issues.splice(newIndex, 0, issueToMove);
+
+ draftData[namespace].issues.nodes = issues;
+ });
+
+ cache.writeQuery({ query: getIssuesQuery, variables, data });
+ },
+ },
+};
+
+export const gqlClient = createDefaultClient(resolvers);
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 93333c31b34..5e04dd1971c 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -1,12 +1,11 @@
-import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
-import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
+import { gqlClient } from './graphql';
export function mountJiraIssuesListApp() {
const el = document.querySelector('.js-jira-issues-import-status');
@@ -56,26 +55,6 @@ export function mountIssuesListApp() {
Vue.use(VueApollo);
Vue.use(VueRouter);
- const resolvers = {
- Mutation: {
- reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
- const variables = JSON.parse(serializedVariables);
- const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
-
- const data = produce(sourceData, (draftData) => {
- const issues = draftData[namespace].issues.nodes.slice();
- const issueToMove = issues[oldIndex];
- issues.splice(oldIndex, 1);
- issues.splice(newIndex, 0, issueToMove);
-
- draftData[namespace].issues.nodes = issues;
- });
-
- cache.writeQuery({ query: getIssuesQuery, variables, data });
- },
- },
- };
-
const {
autocompleteAwardEmojisPath,
calendarPath,
@@ -97,7 +76,6 @@ export function mountIssuesListApp() {
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasIterationsFeature,
- hasMultipleIssueAssigneesFeature,
hasScopedLabelsFeature,
importCsvIssuesPath,
initialEmail,
@@ -125,7 +103,7 @@ export function mountIssuesListApp() {
el,
name: 'IssuesListRoot',
apolloProvider: new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: gqlClient,
}),
router: new VueRouter({
base: window.location.pathname,
@@ -148,7 +126,6 @@ export function mountIssuesListApp() {
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
- hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index df7016aeb74..b447289b425 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -24,6 +24,7 @@ query getIssues(
$crmContactId: String
$crmOrganizationId: String
$not: NegatedIssueFilterInput
+ $or: UnionedIssueFilterInput
$beforeCursor: String
$afterCursor: String
$firstPageSize: Int
@@ -49,6 +50,7 @@ query getIssues(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
@@ -84,6 +86,7 @@ query getIssues(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
before: $beforeCursor
after: $afterCursor
first: $firstPageSize
diff --git a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
index c1aee772167..fdb0eeb5970 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
@@ -17,6 +17,7 @@ query getIssuesCount(
$crmContactId: String
$crmOrganizationId: String
$not: NegatedIssueFilterInput
+ $or: UnionedIssueFilterInput
) {
group(fullPath: $fullPath) @skip(if: $isProject) {
id
@@ -37,6 +38,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -57,6 +59,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -77,6 +80,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -101,6 +105,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -122,6 +127,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
@@ -143,6 +149,7 @@ query getIssuesCount(
crmContactId: $crmContactId
crmOrganizationId: $crmOrganizationId
not: $not
+ or: $or
) {
count
}
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index f02c7a23f51..2f9ab9d62ee 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -5,6 +5,13 @@ import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS_NOT,
+ OPERATOR_OR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_ITERATION,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
API_PARAM,
@@ -31,12 +38,6 @@ import {
specialFilterValues,
TITLE_ASC,
TITLE_DESC,
- TOKEN_TYPE_ASSIGNEE,
- TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_ITERATION,
- TOKEN_TYPE_MILESTONE,
- TOKEN_TYPE_RELEASE,
- TOKEN_TYPE_TYPE,
UPDATED_ASC,
UPDATED_DESC,
URL_PARAM,
@@ -252,20 +253,36 @@ const formatData = (token) => {
export const convertToApiParams = (filterTokens) => {
const params = {};
const not = {};
+ const or = {};
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
const filterType = getFilterType(token.value.data, token.type);
const field = filters[token.type][API_PARAM][filterType];
- const obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
+ let obj;
+ if (token.value.operator === OPERATOR_IS_NOT) {
+ obj = not;
+ } else if (token.value.operator === OPERATOR_OR) {
+ obj = or;
+ } else {
+ obj = params;
+ }
const data = formatData(token);
Object.assign(obj, {
[field]: obj[field] ? [obj[field], data].flat() : data,
});
});
- return Object.keys(not).length ? Object.assign(params, { not }) : params;
+ if (Object.keys(not).length) {
+ Object.assign(params, { not });
+ }
+
+ if (Object.keys(or).length) {
+ Object.assign(params, { or });
+ }
+
+ return params;
};
export const convertToUrlParams = (filterTokens) =>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index dbe634e7295..180dea77003 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -67,7 +67,7 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:enable-autocomplete="enableAutocomplete"
supports-quick-actions
- init-on-autofocus
+ autofocus
@input="$emit('input', $event)"
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable"
diff --git a/app/assets/javascripts/issues/show/components/fields/type.vue b/app/assets/javascripts/issues/show/components/fields/type.vue
index 75d0b9e5e76..5695efd7114 100644
--- a/app/assets/javascripts/issues/show/components/fields/type.vue
+++ b/app/assets/javascripts/issues/show/components/fields/type.vue
@@ -42,7 +42,7 @@ export default {
const {
issueState: { issueType },
} = this;
- return capitalize(issueType);
+ return issuableTypes.find((type) => type.value === issueType)?.text || capitalize(issueType);
},
shouldShowIncident() {
return this.issueType === INCIDENT_TYPE || this.canCreateIncident;
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 74d166f82bb..c01de63ced9 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -7,6 +7,7 @@ import {
GlLink,
GlModal,
GlModalDirective,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
@@ -51,6 +52,7 @@ export default {
},
directives: {
GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin],
inject: {
@@ -287,12 +289,15 @@ export default {
<gl-dropdown
v-if="hasDesktopDropdown"
+ v-gl-tooltip.hover
class="gl-display-none gl-sm-display-inline-flex! gl-ml-3"
icon="ellipsis_v"
category="tertiary"
data-qa-selector="issue_actions_ellipsis_dropdown"
:text="dropdownText"
:text-sr-only="true"
+ :title="dropdownText"
+ :aria-label="dropdownText"
data-testid="desktop-dropdown"
no-caret
right
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index aa7b9805b5f..db846009409 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -1,4 +1,4 @@
-import { __, s__ } from '~/locale';
+import { __, n__, s__ } from '~/locale';
export const timelineTabI18n = Object.freeze({
title: s__('Incident|Timeline'),
@@ -15,6 +15,8 @@ export const timelineFormI18n = Object.freeze({
save: __('Save'),
cancel: __('Cancel'),
description: __('Description'),
+ hint: __('You can enter up to 280 characters'),
+ textRemaining: (count) => n__('%d character remaining', '%d characters remaining', count),
saveAndAdd: s__('Incident|Save and add another event'),
areaLabel: s__('Incident|Timeline text'),
});
@@ -38,3 +40,5 @@ export const timelineItemI18n = Object.freeze({
moreActions: __('More actions'),
timeUTC: __('%{time} UTC'),
});
+
+export const MAX_TEXT_LENGTH = 280;
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 55cd8b5f606..72dfccca467 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -2,7 +2,7 @@
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-import { timelineFormI18n } from './constants';
+import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
export default {
@@ -26,6 +26,7 @@ export default {
GlButton,
},
i18n: timelineFormI18n,
+ MAX_TEXT_LENGTH,
directives: {
autofocusonshow,
},
@@ -63,6 +64,9 @@ export default {
};
},
computed: {
+ isTimelineTextValid() {
+ return this.timelineTextCount > 0 && this.timelineTextRemainingCount >= 0;
+ },
occurredAtString() {
const year = this.datePickerInput.getFullYear();
const month = this.datePickerInput.getMonth();
@@ -74,8 +78,11 @@ export default {
return utcDate.toISOString();
},
- hasTimelineText() {
- return this.timelineText.length > 0;
+ timelineTextRemainingCount() {
+ return MAX_TEXT_LENGTH - this.timelineTextCount;
+ },
+ timelineTextCount() {
+ return this.timelineText.length;
},
},
mounted() {
@@ -158,9 +165,21 @@ export default {
dir="auto"
data-supports-quick-actions="false"
:aria-label="$options.i18n.description"
+ aria-describedby="timeline-form-hint"
:placeholder="$options.i18n.areaPlaceholder"
+ :maxlength="$options.MAX_TEXT_LENGTH"
>
</textarea>
+ <div id="timeline-form-hint" class="gl-sr-only">{{ $options.i18n.hint }}</div>
+ <div
+ aria-hidden="true"
+ class="gl-absolute gl-text-gray-500 gl-font-sm gl-line-height-14 gl-right-4 gl-bottom-3"
+ >
+ {{ timelineTextRemainingCount }}
+ </div>
+ <div role="status" class="gl-sr-only">
+ {{ $options.i18n.textRemaining(timelineTextRemainingCount) }}
+ </div>
</template>
</markdown-field>
</gl-form-group>
@@ -171,7 +190,7 @@ export default {
category="primary"
class="gl-mr-3"
data-testid="save-button"
- :disabled="!hasTimelineText"
+ :disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@click="handleSave(false)"
>
@@ -183,7 +202,7 @@ export default {
category="secondary"
class="gl-mr-3 gl-ml-n2"
data-testid="save-and-add-button"
- :disabled="!hasTimelineText"
+ :disabled="!isTimelineTextValid"
:loading="isEventProcessed"
@click="handleSave(true)"
>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
index 263b2d141c9..64b497c3550 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
@@ -35,6 +35,11 @@ export default {
retryButtonCategory() {
return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
},
+ buttonTitle() {
+ return this.job.status && this.job.status.text === 'passed'
+ ? this.$options.i18n.runAgainJobButtonLabel
+ : this.$options.i18n.retryJobButtonLabel;
+ },
},
methods: {
...mapActions(['toggleSidebar']),
@@ -66,8 +71,8 @@ export default {
<job-sidebar-retry-button
v-if="job.retry_path"
v-gl-tooltip.left
- :title="$options.i18n.retryJobButtonLabel"
- :aria-label="$options.i18n.retryJobButtonLabel"
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
:category="retryButtonCategory"
:href="job.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
index b0db48df01f..aac6a0ad6d3 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
@@ -63,10 +63,23 @@ export default {
commit() {
return this.job?.pipeline?.commit || {};
},
+ selectedStageData() {
+ return this.stages.find((val) => val.name === this.selectedStage);
+ },
shouldShowJobRetryForwardDeploymentModal() {
return this.job.retry_path && this.hasForwardDeploymentFailure;
},
},
+ watch: {
+ job(value, oldValue) {
+ const hasNewStatus = value.status.text !== oldValue.status.text;
+ const isCurrentStage = value?.stage === this.selectedStage;
+
+ if (hasNewStatus && isCurrentStage) {
+ this.fetchJobsForStage(this.selectedStageData);
+ }
+ },
+ },
methods: {
...mapActions(['fetchJobsForStage']),
},
diff --git a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue b/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
index 120f01db8f0..d1b2da4d115 100644
--- a/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/duration_cell.vue
@@ -1,15 +1,16 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { formatDate, getTimeago, durationTimeFormatted } from '~/lib/utils/datetime_utility';
+import { GlIcon } from '@gitlab/ui';
+import { durationTimeFormatted } from '~/lib/utils/datetime_utility';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
iconSize: 12,
- directives: {
- GlTooltip: GlTooltipDirective,
- },
components: {
GlIcon,
+ TimeAgoTooltip,
},
+ mixins: [timeagoMixin],
props: {
job: {
type: Object,
@@ -23,12 +24,6 @@ export default {
duration() {
return this.job?.duration;
},
- timeFormatted() {
- return getTimeago().format(this.finishedTime);
- },
- tooltipTitle() {
- return formatDate(this.finishedTime);
- },
durationFormatted() {
return durationTimeFormatted(this.duration);
},
@@ -44,15 +39,7 @@ export default {
</div>
<div v-if="finishedTime" data-testid="job-finished-time">
<gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
- <time
- v-gl-tooltip
- :title="tooltipTitle"
- :datetime="finishedTime"
- data-placement="top"
- data-container="body"
- >
- {{ timeFormatted }}
- </time>
+ <time-ago-tooltip :time="finishedTime" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index 50ee7bd20dd..e9475994e8b 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -15,6 +15,7 @@ export const JOB_SIDEBAR_COPY = {
retry: __('Retry'),
retryJobButtonLabel: s__('Job|Retry'),
toggleSidebar: __('Toggle Sidebar'),
+ runAgainJobButtonLabel: s__('Job|Run again'),
};
export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
index ba801082377..36f387205f8 100644
--- a/app/assets/javascripts/lazy_loader.js
+++ b/app/assets/javascripts/lazy_loader.js
@@ -169,7 +169,8 @@ export default class LazyLoader {
delete img.dataset.src;
img.classList.remove('lazy');
img.classList.add('js-lazy-loaded');
- img.classList.add('qa-js-lazy-loaded');
+ // eslint-disable-next-line no-param-reassign
+ img.dataset.qa_selector = 'js_lazy_loaded_content';
}
}
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 4448a106bb6..beced4f9144 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -634,66 +634,6 @@ export const NavigationType = {
};
/**
- * Method to perform case-insensitive search for a string
- * within multiple properties and return object containing
- * properties in case there are multiple matches or `null`
- * if there's no match.
- *
- * Eg; Suppose we want to allow user to search using for a string
- * within `iid`, `title`, `url` or `reference` props of a target object;
- *
- * const objectToSearch = {
- * "iid": 1,
- * "title": "Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate. &3",
- * "url": "/groups/gitlab-org/-/epics/1",
- * "reference": "&1",
- * };
- *
- * Following is how we call searchBy and the return values it will yield;
- *
- * - `searchBy('omnis', objectToSearch);`: This will return `{ title: ... }` as our
- * query was found within title prop we only return that.
- * - `searchBy('1', objectToSearch);`: This will return `{ "iid": ..., "reference": ..., "url": ... }`.
- * - `searchBy('https://gitlab.com/groups/gitlab-org/-/epics/1', objectToSearch);`:
- * This will return `{ "url": ... }`.
- * - `searchBy('foo', objectToSearch);`: This will return `null` as no property value
- * matched with our query.
- *
- * You can learn more about behaviour of this method by referring to tests
- * within `spec/frontend/lib/utils/common_utils_spec.js`.
- *
- * @param {string} query String to search for
- * @param {object} searchSpace Object containing properties to search in for `query`
- */
-export const searchBy = (query = '', searchSpace = {}) => {
- const targetKeys = searchSpace !== null ? Object.keys(searchSpace) : [];
-
- if (!query || !targetKeys.length) {
- return null;
- }
-
- const normalizedQuery = query.toLowerCase();
- const matches = targetKeys
- .filter((item) => {
- const searchItem = `${searchSpace[item]}`.toLowerCase();
-
- return (
- searchItem.indexOf(normalizedQuery) > -1 ||
- normalizedQuery.indexOf(searchItem) > -1 ||
- normalizedQuery === searchItem
- );
- })
- .reduce((acc, prop) => {
- const match = acc;
- match[prop] = searchSpace[prop];
-
- return acc;
- }, {});
-
- return Object.keys(matches).length ? matches : null;
-};
-
-/**
* Checks if the given Label has a special syntax `::` in
* it's title.
*
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
new file mode 100644
index 00000000000..3bfbfea7f22
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_action.js
@@ -0,0 +1,57 @@
+import Vue from 'vue';
+
+export function confirmAction(
+ message,
+ {
+ primaryBtnVariant,
+ primaryBtnText,
+ secondaryBtnVariant,
+ secondaryBtnText,
+ cancelBtnVariant,
+ cancelBtnText,
+ modalHtmlMessage,
+ title,
+ hideCancel,
+ } = {},
+) {
+ return new Promise((resolve) => {
+ let confirmed = false;
+ let component;
+
+ const ConfirmAction = Vue.extend({
+ components: {
+ ConfirmModal: () => import('./confirm_modal.vue'),
+ },
+ render(h) {
+ return h(
+ 'confirm-modal',
+ {
+ props: {
+ secondaryText: secondaryBtnText,
+ secondaryVariant: secondaryBtnVariant,
+ primaryVariant: primaryBtnVariant,
+ primaryText: primaryBtnText,
+ cancelVariant: cancelBtnVariant,
+ cancelText: cancelBtnText,
+ title,
+ modalHtmlMessage,
+ hideCancel,
+ },
+ on: {
+ confirmed() {
+ confirmed = true;
+ },
+ closed() {
+ component.$destroy();
+ resolve(confirmed);
+ },
+ },
+ },
+ [message],
+ );
+ },
+ });
+
+ component = new ConfirmAction().$mount();
+ });
+}
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
index 2dc479db80a..0e959e899e9 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js
@@ -1,75 +1,21 @@
-import Vue from 'vue';
+import { confirmAction } from './confirm_action';
-export function confirmAction(
- message,
- {
- primaryBtnVariant,
- primaryBtnText,
- secondaryBtnVariant,
- secondaryBtnText,
- cancelBtnVariant,
- cancelBtnText,
- modalHtmlMessage,
- title,
- hideCancel,
- } = {},
-) {
- return new Promise((resolve) => {
- let confirmed = false;
-
- const component = new Vue({
- components: {
- ConfirmModal: () => import('./confirm_modal.vue'),
- },
- render(h) {
- return h(
- 'confirm-modal',
- {
- props: {
- secondaryText: secondaryBtnText,
- secondaryVariant: secondaryBtnVariant,
- primaryVariant: primaryBtnVariant,
- primaryText: primaryBtnText,
- cancelVariant: cancelBtnVariant,
- cancelText: cancelBtnText,
- title,
- modalHtmlMessage,
- hideCancel,
- },
- on: {
- confirmed() {
- confirmed = true;
- },
- closed() {
- component.$destroy();
- resolve(confirmed);
- },
- },
- },
- [message],
- );
- },
- }).$mount();
- });
-}
-
-export function confirmViaGlModal(message, element) {
- const primaryBtnConfig = {};
-
- const { confirmBtnVariant } = element.dataset;
-
- if (confirmBtnVariant) {
- primaryBtnConfig.primaryBtnVariant = confirmBtnVariant;
- }
+function confirmViaGlModal(message, element) {
+ const { confirmBtnVariant, title, isHtmlMessage } = element.dataset;
const screenReaderText =
element.querySelector('.gl-sr-only')?.textContent ||
element.querySelector('.sr-only')?.textContent ||
element.getAttribute('aria-label');
- if (screenReaderText) {
- primaryBtnConfig.primaryBtnText = screenReaderText;
- }
+ const config = {
+ ...(screenReaderText && { primaryBtnText: screenReaderText }),
+ ...(confirmBtnVariant && { primaryBtnVariant: confirmBtnVariant }),
+ ...(title && { title }),
+ ...(isHtmlMessage && { modalHtmlMessage: message }),
+ };
- return confirmAction(message, primaryBtnConfig);
+ return confirmAction(message, config);
}
+
+export { confirmAction, confirmViaGlModal };
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 c11cf1a7882..4e0a59d0a38 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -271,6 +271,25 @@ export const secondsToMilliseconds = (seconds) => seconds * 1000;
export const secondsToDays = (seconds) => Math.round(seconds / 86400);
/**
+ * Returns the date `n` seconds after the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfSeconds number of seconds after
+ * @return {Date} A `Date` object `n` seconds after the provided `Date`
+ */
+export const nSecondsAfter = (date, numberOfSeconds) =>
+ new Date(date.getTime() + numberOfSeconds * 1000);
+
+/**
+ * Returns the date `n` seconds before the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfSeconds number of seconds before
+ * @return {Date} A `Date` object `n` seconds before the provided `Date`
+ */
+export const nSecondsBefore = (date, numberOfSeconds) => nSecondsAfter(date, -numberOfSeconds);
+
+/**
* Returns the date `n` days after the date provided
*
* @param {Date} date the initial date
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index bca6978c206..cafee641174 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -106,3 +106,15 @@ export const setAttributes = (el, attributes) => {
el.setAttribute(key, attributes[key]);
});
};
+
+/**
+ * Get the height of the wrapper page element
+ * This height can be used to determine where the highest element goes in a page
+ * Useful for gl-drawer's header-height prop
+ * @param {String} contentWrapperClass the content wrapper class
+ * @returns {String} height in px
+ */
+export const getContentWrapperHeight = (contentWrapperClass) => {
+ const wrapperEl = document.querySelector(contentWrapperClass);
+ return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
+};
diff --git a/app/assets/javascripts/lib/utils/unit_format/index.js b/app/assets/javascripts/lib/utils/unit_format/index.js
index 5c5210027e4..bec7e48addc 100644
--- a/app/assets/javascripts/lib/utils/unit_format/index.js
+++ b/app/assets/javascripts/lib/utils/unit_format/index.js
@@ -22,6 +22,7 @@ export const SUPPORTED_FORMATS = {
percentHundred: 'percentHundred',
// Duration
+ days: 'days',
seconds: 'seconds',
milliseconds: 'milliseconds',
@@ -65,6 +66,9 @@ export const getFormatter = (format = SUPPORTED_FORMATS.engineering) => {
}
// Durations
+ if (format === SUPPORTED_FORMATS.days) {
+ return suffixFormatter(s__('Units|d'));
+ }
if (format === SUPPORTED_FORMATS.seconds) {
return suffixFormatter(s__('Units|s'));
}
@@ -161,6 +165,19 @@ export const percent = getFormatter(SUPPORTED_FORMATS.percent);
export const percentHundred = getFormatter(SUPPORTED_FORMATS.percentHundred);
/**
+ * Formats a number of days
+ *
+ * @function
+ * @param {Number} value - Number to format, `1` is rendered as `1d`
+ * @param {Object} options - Formatting options
+ * @param {Number} options.fractionDigits - number of precision decimals
+ * @param {Number} options.maxLength - Max length of formatted number
+ * if length is exceeded, exponential format is used.
+ * @param {String} options.unitSeparator - Separator between value and unit
+ */
+export const days = getFormatter(SUPPORTED_FORMATS.days);
+
+/**
* Formats a number of seconds
*
* @function
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index ca90eee69c7..b1a0baf8150 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -178,7 +178,7 @@ export function mergeUrlParams(params, url, options = {}) {
const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged);
const newQuery = mergedKeys
- .filter((key) => merged[key] !== null)
+ .filter((key) => merged[key] !== null && merged[key] !== undefined)
.map((key) => {
let value = merged[key];
const encodedKey = encodeURIComponent(key);
diff --git a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
index d092283338c..f4893721b9e 100644
--- a/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue
@@ -7,15 +7,12 @@ import RemoveMemberButton from './remove_member_button.vue';
export default {
name: 'AccessRequestActionButtons',
components: { ActionButtonGroup, RemoveMemberButton, ApproveAccessRequestButton },
+ inheritAttrs: false,
props: {
member: {
type: Object,
required: true,
},
- permissions: {
- type: Object,
- required: true,
- },
isCurrentUser: {
type: Boolean,
required: true,
@@ -43,10 +40,10 @@ export default {
<template>
<action-button-group>
- <div v-if="permissions.canUpdate" class="gl-px-1">
+ <div class="gl-px-1">
<approve-access-request-button :member-id="member.id" />
</div>
- <div v-if="permissions.canRemove" class="gl-px-1">
+ <div class="gl-px-1">
<remove-member-button
:member-id="member.id"
:message="message"
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index b824a013f3b..5ac8b30614d 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -27,13 +27,13 @@ export const TABS = [
{
namespace: MEMBER_TYPES.invite,
title: __('Invited'),
- canManageMembersPermissionsRequired: true,
+ requiredPermissions: ['canManageMembers'],
queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
},
{
namespace: MEMBER_TYPES.accessRequest,
title: __('Access requests'),
- canManageMembersPermissionsRequired: true,
+ requiredPermissions: ['canManageAccessRequests'],
queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
},
...EE_TABS,
@@ -44,7 +44,7 @@ export default {
ACTIVE_TAB_QUERY_PARAM_NAME,
TABS,
components: { MembersApp, GlTabs, GlTab, GlBadge, GlButton },
- inject: ['canManageMembers', 'canExportMembers', 'exportCsvPath'],
+ inject: ['canManageMembers', 'canManageAccessRequests', 'canExportMembers', 'exportCsvPath'],
data() {
return {
selectedTabIndex: 0,
@@ -96,15 +96,13 @@ export default {
return true;
}
- const { canManageMembersPermissionsRequired = false } = tab;
+ const { requiredPermissions = [] } = tab;
const tabCanBeShown =
this.getTabCount(tab) > 0 || this.activeTabIndexCalculatedFromUrlParams === index;
- if (canManageMembersPermissionsRequired) {
- return this.canManageMembers && tabCanBeShown;
- }
-
- return tabCanBeShown;
+ return (
+ tabCanBeShown && requiredPermissions.every((requiredPermission) => this[requiredPermission])
+ );
},
},
};
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 34660f8f499..359239c5c0c 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -17,6 +17,7 @@ export const initMembersApp = (el, options) => {
const {
sourceId,
canManageMembers,
+ canManageAccessRequests,
canExportMembers,
canFilterByEnterprise,
exportCsvPath,
@@ -61,6 +62,7 @@ export const initMembersApp = (el, options) => {
currentUserId: gon.current_user_id || null,
sourceId,
canManageMembers,
+ canManageAccessRequests,
canFilterByEnterprise,
canExportMembers,
exportCsvPath,
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 17ee2a0d8b6..0ddf5def8ee 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -260,7 +260,7 @@ export default class MergeRequestTabs {
}
}
- tabShown(action, href) {
+ tabShown(action, href, shouldScroll = true) {
if (action !== this.currentTab && this.mergeRequestTabs) {
this.currentTab = action;
@@ -289,7 +289,9 @@ export default class MergeRequestTabs {
}
if (action === 'commits') {
- this.loadCommits(href);
+ if (!this.commitsLoaded) {
+ this.loadCommits(href);
+ }
// this.hideSidebar();
this.resetViewContainer();
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
@@ -334,7 +336,7 @@ export default class MergeRequestTabs {
$('.detail-page-description').renderGFM();
- this.recallScroll(action);
+ if (shouldScroll) this.recallScroll(action);
} else if (action === this.currentAction) {
// ContentTop is used to handle anything at the top of the page before the main content
const mainContentContainer = document.querySelector('.content-wrapper');
@@ -348,7 +350,7 @@ export default class MergeRequestTabs {
const scrollDestination = tabContentTop - mainContentTop - 51;
// scrollBehavior is only available in browsers that support scrollToOptions
- if ('scrollBehavior' in document.documentElement.style) {
+ if ('scrollBehavior' in document.documentElement.style && shouldScroll) {
window.scrollTo({
top: scrollDestination,
behavior: 'smooth',
@@ -423,28 +425,39 @@ export default class MergeRequestTabs {
return this.currentAction;
}
- loadCommits(source) {
- if (this.commitsLoaded) {
- return;
- }
-
+ loadCommits(source, page = 1) {
toggleLoader(true);
axios
- .get(`${source}.json`)
+ .get(`${source}.json`, { params: { page, per_page: 100 } })
.then(({ data }) => {
+ toggleLoader(false);
+
const commitsDiv = document.querySelector('div#commits');
// eslint-disable-next-line no-unsanitized/property
- commitsDiv.innerHTML = data.html;
+ commitsDiv.innerHTML += data.html;
localTimeAgo(commitsDiv.querySelectorAll('.js-timeago'));
this.commitsLoaded = true;
scrollToContainer('#commits');
- toggleLoader(false);
+ const loadMoreButton = document.querySelector('.js-load-more-commits');
+
+ if (loadMoreButton) {
+ loadMoreButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ loadMoreButton.remove();
+ this.loadCommits(source, loadMoreButton.dataset.nextPage);
+ });
+ }
+
+ if (!data.next_page) {
+ return import('./add_context_commits_modal');
+ }
- return import('./add_context_commits_modal');
+ return null;
})
- .then((m) => m.default())
+ .then((m) => m?.default())
.catch(() => {
toggleLoader(false);
createAlert({
diff --git a/app/assets/javascripts/milestones/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js
deleted file mode 100644
index d4876c3dbe8..00000000000
--- a/app/assets/javascripts/milestones/milestone_select.js
+++ /dev/null
@@ -1,273 +0,0 @@
-/* eslint-disable one-var, no-self-compare, consistent-return, no-param-reassign, no-shadow */
-/* global Issuable */
-
-import $ from 'jquery';
-import { template, escape } from 'lodash';
-import Api from '~/api';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { __, sprintf } from '~/locale';
-import { sortMilestonesByDueDate } from '~/milestones/utils';
-import axios from '~/lib/utils/axios_utils';
-import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
-
-export default class MilestoneSelect {
- constructor(currentProject, els, options = {}) {
- if (currentProject !== null) {
- this.currentProject =
- typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject;
- }
-
- MilestoneSelect.init(els, options);
- }
-
- static init(els, options) {
- let $els = $(els);
-
- if (!els) {
- $els = $('.js-milestone-select');
- }
-
- $els.each((i, dropdown) => {
- let milestoneLinkNoneTemplate,
- milestoneLinkTemplate,
- milestoneExpiredLinkTemplate,
- selectedMilestone,
- selectedMilestoneDefault;
- const $dropdown = $(dropdown);
- const issueUpdateURL = $dropdown.data('issueUpdate');
- const showNo = $dropdown.data('showNo');
- const showAny = $dropdown.data('showAny');
- const showMenuAbove = $dropdown.data('showMenuAbove');
- const showUpcoming = $dropdown.data('showUpcoming');
- const showStarted = $dropdown.data('showStarted');
- const useId = $dropdown.data('useId');
- const defaultLabel = $dropdown.data('defaultLabel');
- const defaultNo = $dropdown.data('defaultNo');
- const abilityName = $dropdown.data('abilityName');
- const $selectBox = $dropdown.closest('.selectbox');
- const $block = $selectBox.closest('.block');
- const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
- const $value = $block.find('.value');
- const $loading = $block.find('.block-loading').addClass('gl-display-none');
- selectedMilestoneDefault = showAny ? '' : null;
- selectedMilestoneDefault =
- showNo && defaultNo ? __('No milestone') : selectedMilestoneDefault;
- selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
-
- if (issueUpdateURL) {
- milestoneLinkTemplate = template(
- '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
- );
- milestoneExpiredLinkTemplate = template(
- '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %> (Past due)</a>',
- );
- milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
- }
- return initDeprecatedJQueryDropdown($dropdown, {
- showMenuAbove,
- data: (term, callback) => {
- let contextId = parseInt($dropdown.get(0).dataset.projectId, 10);
- let getMilestones = Api.projectMilestones.bind(Api);
- const reqParams = { state: 'active', include_parent_milestones: true };
-
- if (term) {
- reqParams.search = term.trim();
- }
-
- if (!contextId) {
- contextId = $dropdown.get(0).dataset.groupId;
- delete reqParams.include_parent_milestones;
- getMilestones = Api.groupMilestones.bind(Api);
- }
-
- // We don't use $.data() as it caches initial value and never updates!
- return getMilestones(contextId, reqParams)
- .then(({ data }) =>
- data
- .map((m) => ({
- ...m,
- // Public API includes `title` instead of `name`.
- name: m.title,
- }))
- .sort(sortMilestonesByDueDate),
- )
- .then((data) => {
- const extraOptions = [];
- if (showAny) {
- extraOptions.push({
- id: null,
- name: null,
- title: __('Any milestone'),
- });
- }
- if (showNo && term.trim() === '') {
- extraOptions.push({
- id: -1,
- name: __('No milestone'),
- title: __('No milestone'),
- });
- }
- if (showUpcoming) {
- extraOptions.push({
- id: -2,
- name: '#upcoming',
- title: __('Upcoming'),
- });
- }
- if (showStarted) {
- extraOptions.push({
- id: -3,
- name: '#started',
- title: __('Started'),
- });
- }
- if (extraOptions.length && data.length) {
- extraOptions.push({ type: 'divider' });
- }
-
- callback(extraOptions.concat(data));
- if (showMenuAbove) {
- $dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
- }
- $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
- });
- },
- renderRow: (milestone) => {
- const milestoneName = milestone.title || milestone.name;
- let milestoneDisplayName = escape(milestoneName);
-
- if (milestone.expired) {
- milestoneDisplayName = sprintf(__('%{milestone} (expired)'), {
- milestone: milestoneDisplayName,
- });
- }
-
- return `
- <li data-milestone-id="${escape(milestoneName)}">
- <a href='#' class='dropdown-menu-milestone-link'>
- ${milestoneDisplayName}
- </a>
- </li>
- `;
- },
- filterable: true,
- filterRemote: true,
- search: {
- fields: ['title'],
- },
- selectable: true,
- toggleLabel: (selected, el) => {
- if (selected && 'id' in selected && $(el).hasClass('is-active')) {
- return selected.title;
- }
- return defaultLabel;
- },
- defaultLabel,
- fieldName: $dropdown.data('fieldName'),
- text: (milestone) => escape(milestone.title),
- id: (milestone) => {
- if (milestone !== undefined) {
- if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
- return milestone.name;
- }
-
- return milestone.id;
- }
- },
- hidden: () => {
- $selectBox.hide();
- // display:block overrides the hide-collapse rule
- return $value.css('display', '');
- },
- opened: (e) => {
- const $el = $(e.currentTarget);
- if (options.handleClick) {
- selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
- }
- $('a.is-active', $el).removeClass('is-active');
- $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
- },
- vue: false,
- clicked: (clickEvent) => {
- const { e } = clickEvent;
- let selected = clickEvent.selectedObj;
-
- if (!selected) return;
-
- if (options.handleClick) {
- e.preventDefault();
- options.handleClick(selected);
- return;
- }
-
- const page = $('body').attr('data-page');
- const isIssueIndex = page === 'projects:issues:index';
- const isMRIndex = page === page && page === 'projects:merge_requests:index';
- const isSelecting = selected.name !== selectedMilestone;
- selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
-
- if (
- $dropdown.hasClass('js-filter-bulk-update') ||
- $dropdown.hasClass('js-issuable-form-dropdown')
- ) {
- e.preventDefault();
- return;
- }
-
- 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();
- }
-
- selected = $selectBox.find('input[type="hidden"]').val();
-
- const data = {};
- data[abilityName] = {};
- data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.removeClass('gl-display-none');
- $dropdown.trigger('loading.gl.dropdown');
- return axios
- .put(issueUpdateURL, data)
- .then(({ data }) => {
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.addClass('gl-display-none');
- $selectBox.hide();
- $value.css('display', '');
- if (data.milestone != null) {
- data.milestone.remaining = timeFor(data.milestone.due_date);
- data.milestone.name = data.milestone.title;
- $value.html(
- data.milestone.expired
- ? milestoneExpiredLinkTemplate({
- ...data.milestone,
- remaining: sprintf(__('%{due_date} (Past due)'), {
- due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
- }),
- })
- : milestoneLinkTemplate(data.milestone),
- );
- return $sidebarCollapsedValue
- .attr(
- 'data-original-title',
- `${data.milestone.name}<br />${data.milestone.remaining}`,
- )
- .find('span')
- .text(data.milestone.title);
- }
- $value.html(milestoneLinkNoneTemplate);
- return $sidebarCollapsedValue
- .attr('data-original-title', __('Milestone'))
- .find('span')
- .text(__('None'));
- })
- .catch(() => {
- $loading.addClass('gl-display-none');
- });
- },
- });
- });
- }
-}
-
-window.MilestoneSelect = MilestoneSelect;
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue
new file mode 100644
index 00000000000..73cdfbc44b0
--- /dev/null
+++ b/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlTable } from '@gitlab/ui';
+import IncubationAlert from './incubation_alert.vue';
+
+export default {
+ name: 'ShowMlExperiment',
+ components: {
+ GlTable,
+ IncubationAlert,
+ },
+ inject: ['candidates', 'metricNames', 'paramNames'],
+ computed: {
+ fields() {
+ return [...this.paramNames, ...this.metricNames];
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-alert />
+
+ <h3>
+ {{ __('Experiment Candidates') }}
+ </h3>
+
+ <gl-table
+ :fields="fields"
+ :items="candidates"
+ :empty-text="__('This Experiment has no logged Candidates')"
+ show-empty
+ class="gl-mt-0!"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue b/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
new file mode 100644
index 00000000000..51c1e935677
--- /dev/null
+++ b/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlAlert, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ i18n: {
+ titleLabel: __('Machine Learning Experiment Tracking is in Incubating Phase'),
+ contentLabel: __(
+ 'GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited',
+ ),
+ learnMoreLabel: __('Learn More'),
+ feedbackLabel: __('Feedback and Updates'),
+ },
+ name: 'MlopsIncubationAlert',
+ components: { GlAlert, GlLink },
+ data() {
+ return {
+ isAlertDismissed: false,
+ };
+ },
+ computed: {
+ shouldShowAlert() {
+ return !this.isAlertDismissed;
+ },
+ },
+ methods: {
+ dismissAlert() {
+ this.isAlertDismissed = true;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="shouldShowAlert"
+ :title="$options.i18n.titleLabel"
+ variant="warning"
+ :primary-button-text="$options.i18n.feedbackLabel"
+ primary-button-link="https://gitlab.com/groups/gitlab-org/-/epics/8560"
+ @dismiss="dismissAlert"
+ >
+ {{ $options.i18n.contentLabel }}
+ <gl-link href="https://about.gitlab.com/handbook/engineering/incubation/" target="_blank">{{
+ $options.i18n.learnMoreLabel
+ }}</gl-link>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 127e046b5a9..9aa6abd9d8c 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -148,6 +148,11 @@ export default {
type: Object,
required: true,
},
+ hidePrompt: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
markdown() {
@@ -163,7 +168,7 @@ export default {
<template>
<div class="cell text-cell">
- <prompt />
+ <prompt v-if="!hidePrompt" />
<div v-safe-html:[$options.markdownConfig]="markdown" class="markdown"></div>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue
index 88d01ffa659..bd01534089e 100644
--- a/app/assets/javascripts/notebook/cells/output/index.vue
+++ b/app/assets/javascripts/notebook/cells/output/index.vue
@@ -3,6 +3,9 @@ import CodeOutput from '../code/index.vue';
import HtmlOutput from './html.vue';
import ImageOutput from './image.vue';
import LatexOutput from './latex.vue';
+import MarkdownOutput from './markdown.vue';
+
+const TEXT_MARKDOWN = 'text/markdown';
export default {
props: {
@@ -35,6 +38,8 @@ export default {
return 'text/latex';
} else if (output.data['image/svg+xml']) {
return 'image/svg+xml';
+ } else if (output.data[TEXT_MARKDOWN]) {
+ return TEXT_MARKDOWN;
}
return 'text/plain';
@@ -42,7 +47,7 @@ export default {
dataForType(output, type) {
let data = output.data[type];
- if (typeof data === 'object') {
+ if (typeof data === 'object' && this.outputType(output) !== TEXT_MARKDOWN) {
data = data.join('');
}
@@ -61,6 +66,8 @@ export default {
return LatexOutput;
} else if (output.data['image/svg+xml']) {
return HtmlOutput;
+ } else if (output.data[TEXT_MARKDOWN]) {
+ return MarkdownOutput;
}
return CodeOutput;
diff --git a/app/assets/javascripts/notebook/cells/output/markdown.vue b/app/assets/javascripts/notebook/cells/output/markdown.vue
new file mode 100644
index 00000000000..5da057dee72
--- /dev/null
+++ b/app/assets/javascripts/notebook/cells/output/markdown.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import Prompt from '../prompt.vue';
+import Markdown from '../markdown.vue';
+
+export default {
+ name: 'MarkdownOutput',
+ components: {
+ Prompt,
+ Markdown,
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ rawCode: {
+ type: Array,
+ required: true,
+ },
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ markdownContent() {
+ return { source: this.rawCode };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="output">
+ <prompt type="Out" :count="count" :show-output="index === 0" />
+ <markdown :cell="markdownContent" :hide-prompt="true" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 2dbc9b10836..3e8cddc3174 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -52,6 +52,11 @@ export default {
required: false,
default: false,
},
+ shouldScrollToNote: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
...mapGetters(['userCanReply']),
@@ -133,6 +138,7 @@ export default {
:discussion-root="true"
:discussion-resolve-path="discussion.resolve_path"
:is-overview-tab="isOverviewTab"
+ :should-scroll-to-note="shouldScrollToNote"
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
@@ -183,6 +189,7 @@ export default {
:discussion-root="index === 0"
:discussion-resolve-path="discussion.resolve_path"
:is-overview-tab="isOverviewTab"
+ :should-scroll-to-note="shouldScrollToNote"
@handleDeleteNote="$emit('deleteNote')"
>
<template #avatar-badge>
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index f3530344181..63c7010983e 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -198,7 +198,7 @@ export default {
<gl-badge
v-if="isInternalNote"
v-gl-tooltip:tooltipcontainer.bottom
- data-testid="internalNoteIndicator"
+ data-testid="internal-note-indicator"
variant="warning"
size="sm"
class="gl-ml-2"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 50d166b6db5..b668d6ec182 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -73,6 +73,11 @@ export default {
required: false,
default: false,
},
+ shouldScrollToNote: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -288,6 +293,7 @@ export default {
:line="line"
:should-group-replies="shouldGroupReplies"
:is-overview-tab="isOverviewTab"
+ :should-scroll-to-note="shouldScrollToNote"
@startReplying="showReplyForm"
@deleteNote="deleteNoteHandler"
>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index c4b3111b919..8ce0c2f8648 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -91,6 +91,11 @@ export default {
required: false,
default: false,
},
+ shouldScrollToNote: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -222,7 +227,7 @@ export default {
},
mounted() {
- if (this.isTarget) {
+ if (this.isTarget && this.shouldScrollToNote) {
this.scrollToNoteIfNeeded($(this.$el));
}
},
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index e4f88962731..9c3b2139a5d 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -27,7 +27,7 @@ export default {
<template>
<div
- class="gl-display-flex gl-sm-align-items-center gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-pt-5 gl-mt-5 gl-border-t"
+ class="gl-display-flex gl-sm-align-items-center gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-pt-5"
>
<h2 class="gl-font-size-h1 gl-m-0">{{ __('Activity') }}</h2>
<div class="gl-display-flex gl-gap-3 gl-w-full gl-sm-w-auto gl-mt-3 gl-sm-mt-0">
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 9c2ff2c3e7f..7bb1a1a1bfe 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -126,6 +126,9 @@ export default {
slotKeys() {
return this.sortDirDesc ? ['form', 'comments'] : ['comments', 'form'];
},
+ isAppReady() {
+ return !this.isLoading && !this.renderSkeleton && this.shouldShow;
+ },
},
watch: {
async isFetching() {
@@ -149,6 +152,15 @@ export default {
this.discussionsCount.textContent = val;
}
},
+ isAppReady: {
+ handler(isReady) {
+ if (!isReady) return;
+ this.$nextTick(() => {
+ window.mrTabs?.eventHub.$emit('NotesAppReady');
+ });
+ },
+ immediate: true,
+ },
},
created() {
this.discussionsCount = document.querySelector('.js-discussions-count');
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index d75a4158440..3dbcf28d11c 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,8 +1,12 @@
import { mapGetters, mapActions, mapState } from 'vuex';
import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
+function isOverviewPage() {
+ return window.mrTabs?.currentAction === 'show';
+}
+
function getAllDiscussionElements() {
- const containerEl = window.mrTabs?.currentAction === 'diffs' ? '.diffs' : '.notes';
+ const containerEl = isOverviewPage() ? '.tab-pane.notes' : '.diffs';
return Array.from(
document.querySelectorAll(
`${containerEl} div[data-discussion-id]:not([data-discussion-resolved])`,
@@ -59,6 +63,14 @@ function getPreviousDiscussion() {
function handleJumpForBothPages(getDiscussion, ctx, fn, scrollOptions) {
const discussion = getDiscussion();
+ if (!isOverviewPage() && !discussion) {
+ window.mrTabs?.eventHub.$once('NotesAppReady', () => {
+ handleJumpForBothPages(getDiscussion, ctx, fn, scrollOptions);
+ });
+ window.mrTabs?.setCurrentAction('show');
+ window.mrTabs?.tabShown('show', undefined, false);
+ return;
+ }
const id = discussion.dataset.discussionId;
ctx.expandDiscussion({ discussionId: id });
scrollToElement(discussion, scrollOptions);
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 6876220f75c..5ad7a811726 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -94,9 +94,9 @@ export const getUserDataByProp = (state) => (prop) => state.userData && state.us
export const descriptionVersions = (state) => state.descriptionVersions;
export const canUserAddIncidentTimelineEvents = (state) => {
- return (
- state.userData.can_add_timeline_events &&
- state.noteableData.type === constants.NOTEABLE_TYPE_MAPPING.Incident
+ return Boolean(
+ state.userData?.can_add_timeline_events &&
+ state.noteableData.type === constants.NOTEABLE_TYPE_MAPPING.Incident,
);
};
diff --git a/app/assets/javascripts/observability/components/observability_app.vue b/app/assets/javascripts/observability/components/observability_app.vue
new file mode 100644
index 00000000000..4f5e27be46f
--- /dev/null
+++ b/app/assets/javascripts/observability/components/observability_app.vue
@@ -0,0 +1,42 @@
+<script>
+export default {
+ props: {
+ observabilityIframeSrc: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ window.addEventListener('message', this.messageHandler);
+ },
+ methods: {
+ messageHandler(e) {
+ const isExpectedOrigin = e.origin === new URL(this.observabilityIframeSrc)?.origin;
+
+ const isNewObservabilityPath = this.$route?.query?.observability_path !== e.data?.url;
+
+ const shouldNotHandleMessage = !isExpectedOrigin || !e.data.url || !isNewObservabilityPath;
+
+ if (shouldNotHandleMessage) {
+ return;
+ }
+
+ // this will update the `observability_path` query param on each route change inside Observability UI
+ this.$router.replace({
+ name: this.$route.pathname,
+ query: { ...this.$route.query, observability_path: e.data.url },
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <iframe
+ id="observability-ui-iframe"
+ data-testid="observability-ui-iframe"
+ frameborder="0"
+ height="100%"
+ :src="observabilityIframeSrc"
+ ></iframe>
+</template>
diff --git a/app/assets/javascripts/observability/index.js b/app/assets/javascripts/observability/index.js
new file mode 100644
index 00000000000..cd342ebee3e
--- /dev/null
+++ b/app/assets/javascripts/observability/index.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+
+import ObservabilityApp from './components/observability_app.vue';
+
+Vue.use(VueRouter);
+
+export default () => {
+ const el = document.getElementById('js-observability-app');
+
+ if (!el) return false;
+
+ const router = new VueRouter({
+ mode: 'history',
+ });
+
+ return new Vue({
+ el,
+ router,
+ render(h) {
+ return h(ObservabilityApp, {
+ props: {
+ observabilityIframeSrc: el.dataset.observabilityIframeSrc,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
index 80bca536b7c..23d8e97dd79 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue
@@ -3,7 +3,6 @@ import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader, GlButton } fro
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { n__ } from '~/locale';
import Tracking from '~/tracking';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { joinPaths } from '~/lib/utils/url_utility';
@@ -38,7 +37,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
+ mixins: [Tracking.mixin()],
inject: ['config'],
props: {
item: {
@@ -91,17 +90,14 @@ export default {
);
},
imageName() {
- if (this.glFeatures.containerRegistryShowShortenedPath) {
- if (this.showFullPath) {
- return this.item.path;
- }
- const projectPath = this.item?.project?.path?.toLowerCase() ?? '';
- if (this.item.name) {
- return joinPaths(projectPath, this.item.name);
- }
- return projectPath;
+ if (this.showFullPath) {
+ return this.item.path;
}
- return this.item.path;
+ const projectPath = this.item?.project?.path?.toLowerCase() ?? '';
+ if (this.item.name) {
+ return joinPaths(projectPath, this.item.name);
+ }
+ return projectPath;
},
routerLinkEvent() {
return this.deleting ? '' : 'click';
@@ -136,7 +132,7 @@ export default {
>
<template #left-primary>
<gl-button
- v-if="glFeatures.containerRegistryShowShortenedPath && !showFullPath"
+ v-if="!showFullPath"
v-gl-tooltip="{
placement: 'top',
title: $options.i18n.IMAGE_FULL_PATH_LABEL,
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
index 19d35a135fd..ba8caabb40a 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue
@@ -95,13 +95,8 @@ export default {
<template #right-actions>
<slot name="commands"></slot>
</template>
- <template #metadata-count>
- <metadata-item
- v-if="imagesCount"
- data-testid="images-count"
- icon="container-image"
- :text="imagesCountText"
- />
+ <template v-if="imagesCount" #metadata-count>
+ <metadata-item data-testid="images-count" icon="container-image" :text="imagesCountText" />
</template>
<template #metadata-exp-policies>
<metadata-item
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
index 9694bfd4e77..9b062024d03 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js
@@ -4,11 +4,29 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
+export const mergeVariables = (existing, incoming) => {
+ if (!incoming) return existing;
+ if (!existing) return incoming;
+ return incoming;
+};
+
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
batchMax: 1,
+ cacheConfig: {
+ typePolicies: {
+ ContainerRepositoryDetails: {
+ fields: {
+ tags: {
+ keyArgs: ['id'],
+ merge: mergeVariables,
+ },
+ },
+ },
+ },
+ },
},
),
});
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index 8b66165a57a..b339c8c8371 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -31,6 +31,7 @@ import {
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql';
+import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
const REPOSITORY_IMPORTING_ERROR_MESSAGE = 'repository importing';
@@ -145,6 +146,13 @@ export default {
query: getContainerRepositoryTagsQuery,
variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE },
},
+ {
+ query: getContainerRepositoriesDetails,
+ variables: {
+ fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
+ isGroupPage: this.config.isGroupPage,
+ },
+ },
],
});
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
index 8b6a5c59847..707e8f09045 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
@@ -1,8 +1,8 @@
<script>
-import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlPagination } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
-import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants';
@@ -11,8 +11,7 @@ import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registr
export default {
components: {
GlPagination,
- GlModal,
- GlSprintf,
+ DeletePackageModal,
PackagesListLoader,
PackagesListRow,
},
@@ -42,22 +41,6 @@ export default {
isListEmpty() {
return !this.list || this.list.length === 0;
},
- deletePackageName() {
- return this.itemToBeDeleted?.name ?? '';
- },
- deleteModalActionPrimaryProps() {
- return {
- text: this.$options.i18n.modalAction,
- attributes: {
- variant: 'danger',
- },
- };
- },
- deleteModalActionCancelProps() {
- return {
- text: __('Cancel'),
- };
- },
tracking() {
return {
category: TRACK_CATEGORY,
@@ -68,7 +51,6 @@ export default {
setItemToBeDeleted(item) {
this.itemToBeDeleted = { ...item };
this.track(TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE);
- this.$refs.packageListDeleteModal.show();
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
@@ -80,11 +62,6 @@ export default {
this.itemToBeDeleted = null;
},
},
- i18n: {
- deleteModalContent: s__('PackageRegistry|You are about to delete %{name}, are you sure?'),
- modalTitle: s__('PackageRegistry|Delete package'),
- modalAction: s__('PackageRegistry|Permanently delete'),
- },
};
</script>
@@ -116,22 +93,11 @@ export default {
class="gl-w-full gl-mt-3"
/>
- <gl-modal
- ref="packageListDeleteModal"
- size="sm"
- modal-id="confirm-delete-package"
- :action-primary="deleteModalActionPrimaryProps"
- :action-cancel="deleteModalActionCancelProps"
+ <delete-package-modal
+ :item-to-be-deleted="itemToBeDeleted"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
- >
- <template #modal-title>{{ $options.i18n.modalTitle }}</template>
- <gl-sprintf :message="$options.i18n.deleteModalContent">
- <template #name>
- <strong>{{ deletePackageName }}</strong>
- </template>
- </gl-sprintf>
- </gl-modal>
+ />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue
new file mode 100644
index 00000000000..2a1de2ae4a7
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlModal } from '@gitlab/ui';
+import { __, n__ } from '~/locale';
+import {
+ DELETE_PACKAGES_MODAL_TITLE,
+ DELETE_PACKAGE_MODAL_PRIMARY_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
+
+export default {
+ name: 'DeleteModal',
+ i18n: {
+ DELETE_PACKAGES_MODAL_TITLE,
+ },
+ components: {
+ GlModal,
+ },
+ props: {
+ itemsToBeDeleted: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ description() {
+ return n__(
+ 'PackageRegistry|You are about to delete 1 package. This operation is irreversible.',
+ `PackageRegistry|You are about to delete %d packages. This operation is irreversible.`,
+ this.itemsToBeDeleted.length,
+ );
+ },
+ },
+ modal: {
+ packagesDeletePrimaryAction: {
+ text: DELETE_PACKAGE_MODAL_PRIMARY_ACTION,
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ },
+ cancelAction: {
+ text: __('Cancel'),
+ },
+ },
+ methods: {
+ show() {
+ this.$refs.deleteModal.show();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="deleteModal"
+ size="sm"
+ modal-id="delete-packages-modal"
+ :action-primary="$options.modal.packagesDeletePrimaryAction"
+ :action-cancel="$options.modal.cancelAction"
+ :title="$options.i18n.DELETE_PACKAGES_MODAL_TITLE"
+ @primary="$emit('confirm')"
+ >
+ <span>{{ description }}</span>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
new file mode 100644
index 00000000000..efc60c9c037
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+
+export default {
+ components: {
+ VersionRow,
+ GlKeysetPagination,
+ PackagesListLoader,
+ },
+ props: {
+ versions: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ showPagination() {
+ return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
+ },
+ isListEmpty() {
+ return this.versions.length === 0;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="isLoading">
+ <packages-list-loader />
+ </div>
+ <slot v-else-if="isListEmpty" name="empty-state"></slot>
+ <div v-else>
+ <version-row v-for="version in versions" :key="version.id" :package-entity="version" />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ v-bind="pageInfo"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 7a000aca0f2..4553dd3421b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -2,6 +2,7 @@
import {
GlDropdown,
GlDropdownItem,
+ GlFormCheckbox,
GlIcon,
GlSprintf,
GlTooltipDirective,
@@ -26,6 +27,7 @@ export default {
components: {
GlDropdown,
GlDropdownItem,
+ GlFormCheckbox,
GlIcon,
GlSprintf,
GlTruncate,
@@ -45,6 +47,11 @@ export default {
type: Object,
required: true,
},
+ selected: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
packageType() {
@@ -90,7 +97,15 @@ export default {
</script>
<template>
- <list-item data-testid="package-row">
+ <list-item data-testid="package-row" v-bind="$attrs">
+ <template #left-action>
+ <gl-form-checkbox
+ v-if="packageEntity.canDestroy"
+ class="gl-m-0"
+ :checked="selected"
+ @change="$emit('select')"
+ />
+ </template>
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<router-link
@@ -168,12 +183,9 @@ export default {
category="tertiary"
no-caret
>
- <gl-dropdown-item
- data-testid="action-delete"
- variant="danger"
- @click="$emit('packageToDelete', packageEntity)"
- >{{ $options.i18n.deletePackage }}</gl-dropdown-item
- >
+ <gl-dropdown-item data-testid="action-delete" variant="danger" @click="$emit('delete')">{{
+ $options.i18n.deletePackage
+ }}</gl-dropdown-item>
</gl-dropdown>
</template>
</list-item>
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 c6583b8f09f..ddcddf80c15 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
@@ -1,8 +1,10 @@
<script>
-import { GlAlert, GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
+import { GlAlert } from '@gitlab/ui';
+import { s__, sprintf, n__ } from '~/locale';
+import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
@@ -13,13 +15,13 @@ import { packageTypeToTrackCategory } from '~/packages_and_registries/package_re
import Tracking from '~/tracking';
export default {
+ name: 'PackagesList',
components: {
GlAlert,
- GlKeysetPagination,
- GlModal,
- GlSprintf,
+ DeletePackageModal,
PackagesListLoader,
PackagesListRow,
+ RegistryList,
},
mixins: [Tracking.mixin()],
props: {
@@ -46,12 +48,12 @@ export default {
};
},
computed: {
+ listTitle() {
+ return n__('%d package', '%d packages', this.list.length);
+ },
isListEmpty() {
return !this.list || this.list.length === 0;
},
- deletePackageName() {
- return this.itemToBeDeleted?.name ?? '';
- },
tracking() {
const category = this.itemToBeDeleted
? packageTypeToTrackCategory(this.itemToBeDeleted.packageType)
@@ -60,32 +62,6 @@ export default {
category,
};
},
- showPagination() {
- return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
- },
- showDeleteModal: {
- get() {
- return Boolean(this.itemToBeDeleted);
- },
- set(value) {
- if (!value) {
- this.itemToBeDeleted = null;
- }
- },
- },
- deleteModalActionPrimaryProps() {
- return {
- text: this.$options.i18n.modalAction,
- attributes: {
- variant: 'danger',
- },
- };
- },
- deleteModalActionCancelProps() {
- return {
- text: __('Cancel'),
- };
- },
errorTitleAlert() {
return sprintf(
s__('PackageRegistry|There was an error publishing a %{packageName} package'),
@@ -110,21 +86,28 @@ export default {
this.itemToBeDeleted = { ...item };
this.track(REQUEST_DELETE_PACKAGE_TRACKING_ACTION);
},
+ setItemsToBeDeleted(items) {
+ if (items.length === 1) {
+ const [item] = items;
+ this.setItemToBeDeleted(item);
+ return;
+ }
+ this.$emit('delete', items);
+ },
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
this.track(DELETE_PACKAGE_TRACKING_ACTION);
+ this.itemToBeDeleted = null;
},
deleteItemCanceled() {
this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION);
+ this.itemToBeDeleted = null;
},
showConfirmationModal() {
this.setItemToBeDeleted(this.errorPackages[0]);
},
},
i18n: {
- deleteModalContent: s__('PackageRegistry|You are about to delete %{name}, are you sure?'),
- modalTitle: s__('PackageRegistry|Delete package'),
- modalAction: s__('PackageRegistry|Permanently delete'),
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
@@ -150,41 +133,32 @@ export default {
@primaryAction="showConfirmationModal"
>{{ $options.i18n.errorMessageBodyAlert }}</gl-alert
>
- <div data-testid="packages-table">
- <packages-list-row
- v-for="packageEntity in list"
- :key="packageEntity.id"
- :package-entity="packageEntity"
- @packageToDelete="setItemToBeDeleted"
- />
- </div>
-
- <div class="gl-display-flex gl-justify-content-center">
- <gl-keyset-pagination
- v-if="showPagination"
- v-bind="pageInfo"
- class="gl-mt-3"
- @prev="$emit('prev-page')"
- @next="$emit('next-page')"
- />
- </div>
+ <registry-list
+ data-testid="packages-table"
+ :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
+ :first="first"
+ :package-entity="item"
+ :selected="isSelected(item)"
+ @delete="setItemToBeDeleted(item)"
+ @select="selectItem(item)"
+ />
+ </template>
+ </registry-list>
- <gl-modal
- v-model="showDeleteModal"
- modal-id="confirm-delete-package"
- size="sm"
- :action-primary="deleteModalActionPrimaryProps"
- :action-cancel="deleteModalActionCancelProps"
+ <delete-package-modal
+ :item-to-be-deleted="itemToBeDeleted"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
- >
- <template #modal-title>{{ $options.i18n.modalTitle }}</template>
- <gl-sprintf :message="$options.i18n.deleteModalContent">
- <template #name>
- <strong>{{ deletePackageName }}</strong>
- </template>
- </gl-sprintf>
- </gl-modal>
+ />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 4e35176c757..b731cd77e66 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -110,6 +110,13 @@ export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__(
export const FETCH_PACKAGE_METADATA_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while fetching the package metadata.',
);
+export const DELETE_PACKAGES_ERROR_MESSAGE = s__(
+ 'PackageRegistry|Something went wrong while deleting packages.',
+);
+export const DELETE_PACKAGES_SUCCESS_MESSAGE = s__('PackageRegistry|Packages deleted successfully');
+
+export const DELETE_PACKAGES_MODAL_TITLE = s__('PackageRegistry|Delete packages');
+export const DELETE_PACKAGE_MODAL_PRIMARY_ACTION = s__('PackageRegistry|Permanently delete');
export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
export const PACKAGE_REGISTRY_TITLE = __('Package Registry');
@@ -177,6 +184,9 @@ export const PACKAGE_TYPES = [
s__('PackageRegistry|Helm'),
];
+export const HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE = 'hide_package_registry_migration_survey';
+export const SURVEY_LINK = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_cHomH9FPzOaiDTU';
+
// links
export const EMPTY_LIST_HELP_URL = helpPagePath('user/packages/package_registry/index');
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql
new file mode 100644
index 00000000000..e1ff5518bf8
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql
@@ -0,0 +1,5 @@
+mutation destroyPackages($ids: [PackagesPackageID!]!) {
+ destroyPackages(input: { ids: $ids }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 8e50c95b10b..51e0ab5aba8 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -1,4 +1,10 @@
-query getPackageDetails($id: PackagesPackageID!) {
+query getPackageDetails(
+ $id: PackagesPackageID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
package(id: $id) {
id
name
@@ -55,7 +61,7 @@ query getPackageDetails($id: PackagesPackageID!) {
downloadPath
}
}
- versions(first: 100) {
+ versions(after: $after, before: $before, first: $first, last: $last) {
nodes {
id
name
@@ -69,6 +75,12 @@ query getPackageDetails($id: PackagesPackageID!) {
}
}
}
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ endCursor
+ startCursor
+ }
}
dependencyLinks {
nodes {
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 6680e612985..336eb0ca079 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js
@@ -36,6 +36,7 @@ export default () => {
const attachMainComponent = () =>
new Vue({
el,
+ name: 'PackageRegistery',
router,
apolloProvider,
provide: {
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index eeed56b77c3..c59dcaee411 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -22,7 +22,7 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
PACKAGE_TYPE_NUGET,
@@ -48,6 +48,7 @@ import {
DELETE_MODAL_CONTENT,
DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
+ GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
@@ -65,13 +66,13 @@ export default {
GlTabs,
GlSprintf,
PackageTitle,
- VersionRow,
DependencyRow,
PackageHistory,
AdditionalMetadata,
InstallationCommands,
PackageFiles,
DeletePackage,
+ PackageVersionsList,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -132,6 +133,7 @@ export default {
queryVariables() {
return {
id: convertToGraphQLId('Packages::Package', this.packageId),
+ first: GRAPHQL_PAGE_SIZE,
};
},
packageFiles() {
@@ -157,6 +159,9 @@ export default {
hasVersions() {
return this.packageEntity.versions?.nodes?.length > 0;
},
+ versionPageInfo() {
+ return this.packageEntity?.versions?.pageInfo ?? {};
+ },
packageDependencies() {
return this.packageEntity.dependencyLinks?.nodes || [];
},
@@ -264,6 +269,34 @@ export default {
resetDeleteModalContent() {
this.deletePackageModalContent = DELETE_MODAL_CONTENT;
},
+ updateQuery(_, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ fetchPreviousVersionsPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.versionPageInfo?.startCursor,
+ };
+ this.$apollo.queries.packageEntity.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
+ fetchNextVersionsPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: GRAPHQL_PAGE_SIZE,
+ last: null,
+ after: this.versionPageInfo?.endCursor,
+ };
+
+ this.$apollo.queries.packageEntity.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
},
i18n: {
DELETE_MODAL_TITLE,
@@ -271,6 +304,7 @@ export default {
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
+ otherVersionsTabTitle: __('Other versions'),
},
modal: {
packageDeletePrimaryAction: {
@@ -303,7 +337,7 @@ export default {
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="emptyListIllustration"
/>
- <div v-else-if="!isLoading" class="packages-app">
+ <div v-else-if="projectName" class="packages-app">
<package-title :package-entity="packageEntity">
<template #delete-button>
<gl-button
@@ -358,14 +392,20 @@ export default {
</p>
</gl-tab>
- <gl-tab :title="__('Other versions')" title-item-class="js-versions-tab">
- <template v-if="hasVersions">
- <version-row v-for="v in packageEntity.versions.nodes" :key="v.id" :package-entity="v" />
- </template>
-
- <p v-else class="gl-mt-3" data-testid="no-versions-message">
- {{ s__('PackageRegistry|There are no other versions of this package.') }}
- </p>
+ <gl-tab :title="$options.i18n.otherVersionsTabTitle" title-item-class="js-versions-tab" lazy>
+ <package-versions-list
+ :is-loading="isLoading"
+ :page-info="versionPageInfo"
+ :versions="packageEntity.versions.nodes"
+ @prev-page="fetchPreviousVersionsPage"
+ @next-page="fetchNextVersionsPage"
+ >
+ <template #empty-state>
+ <p class="gl-mt-3" data-testid="no-versions-message">
+ {{ s__('PackageRegistry|There are no other versions of this package.') }}
+ </p>
+ </template>
+ </package-versions-list>
</gl-tab>
</gl-tabs>
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 ed9ab0367dd..8b5d51cb856 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
@@ -1,41 +1,52 @@
<script>
-import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlBanner, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { createAlert, VARIANT_INFO } from '~/flash';
-import { historyReplaceState } from '~/lib/utils/common_utils';
+import { getCookie, historyReplaceState, parseBoolean, setCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
+ HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
+ DELETE_PACKAGES_ERROR_MESSAGE,
+ DELETE_PACKAGES_SUCCESS_MESSAGE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
+ SURVEY_LINK,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
-
+import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
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 DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
export default {
components: {
+ GlAlert,
+ GlBanner,
GlEmptyState,
GlLink,
GlSprintf,
PackageList,
PackageTitle,
PackageSearch,
+ DeleteModal,
DeletePackage,
},
inject: ['emptyListIllustration', 'isGroupPage', 'fullPath'],
data() {
return {
+ alertVariables: null,
+ itemsToBeDeleted: [],
packages: {},
sort: '',
filters: {},
mutationLoading: false,
+ showSurveyBanner: !parseBoolean(getCookie(HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE)),
};
},
apollo: {
@@ -110,10 +121,53 @@ export default {
historyReplaceState(cleanUrl);
}
},
+ async confirmDelete() {
+ const { itemsToBeDeleted } = this;
+ this.itemsToBeDeleted = [];
+ this.mutationLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: destroyPackagesMutation,
+ variables: {
+ ids: itemsToBeDeleted.map((i) => i.id),
+ },
+ awaitRefetchQueries: true,
+ refetchQueries: [
+ {
+ query: getPackagesQuery,
+ variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE },
+ },
+ ],
+ });
+
+ if (data?.destroyPackages?.errors[0]) {
+ throw new Error(data.destroyPackages.errors[0]);
+ }
+ this.showAlert({
+ variant: 'success',
+ message: DELETE_PACKAGES_SUCCESS_MESSAGE,
+ });
+ } catch {
+ this.showAlert({
+ variant: 'danger',
+ message: DELETE_PACKAGES_ERROR_MESSAGE,
+ });
+ } finally {
+ this.mutationLoading = false;
+ }
+ },
+ showDeletePackagesModal(toBeDeleted) {
+ this.itemsToBeDeleted = toBeDeleted;
+ this.$refs.deletePackagesModal.show();
+ },
handleSearchUpdate({ sort, filters }) {
this.sort = sort;
this.filters = { ...filters };
},
+ hideSurvey() {
+ this.showSurveyBanner = false;
+ setCookie(HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE, 'true');
+ },
updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult;
},
@@ -143,6 +197,9 @@ export default {
updateQuery: this.updateQuery,
});
},
+ showAlert(obj) {
+ this.alertVariables = { ...obj };
+ },
},
i18n: {
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
@@ -151,18 +208,44 @@ export default {
noResultsText: s__(
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
),
+ surveyBannerTitle: s__('PackageRegistry|Help us learn about your registry migration needs'),
+ surveyBannerDescription: s__(
+ 'PackageRegistry|If you are interested in migrating packages from your private registry to the GitLab Package Registry, take our survey and tell us more about your needs.',
+ ),
+ surveyBannerPrimaryButtonText: s__('PackageRegistry|Take survey'),
},
links: {
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
},
+ surveyLink: SURVEY_LINK,
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="alertVariables"
+ :variant="alertVariables.variant"
+ class="gl-mt-5"
+ dismissible
+ @dismiss="alertVariables = null"
+ >
+ {{ alertVariables.message }}
+ </gl-alert>
+ <gl-banner
+ v-if="showSurveyBanner"
+ :title="$options.i18n.surveyBannerTitle"
+ :button-text="$options.i18n.surveyBannerPrimaryButtonText"
+ :button-link="$options.surveyLink"
+ class="gl-mt-3"
+ @primary="hideSurvey"
+ @close="hideSurvey"
+ >
+ <p>{{ $options.i18n.surveyBannerDescription }}</p>
+ </gl-banner>
<package-title :help-url="$options.links.PACKAGE_HELP_URL" :count="packagesCount" />
- <package-search @update="handleSearchUpdate" />
+ <package-search class="gl-mb-5" @update="handleSearchUpdate" />
<delete-package
:refetch-queries="refetchQueriesData"
@@ -178,6 +261,7 @@ export default {
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
@package:delete="deletePackage"
+ @delete="showDeletePackagesModal"
>
<template #empty-state>
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
@@ -196,5 +280,11 @@ export default {
</package-list>
</template>
</delete-package>
+
+ <delete-modal
+ ref="deletePackagesModal"
+ :items-to-be-deleted="itemsToBeDeleted"
+ @confirm="confirmDelete"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue
new file mode 100644
index 00000000000..c7fddadab1b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlFormCheckbox, GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import {
+ PACKAGE_FORWARDING_CHECKBOX_LABEL,
+ PACKAGE_FORWARDING_ENFORCE_LABEL,
+} from '~/packages_and_registries/settings/group/constants';
+
+export default {
+ name: 'ForwardingSettings',
+ i18n: {
+ PACKAGE_FORWARDING_CHECKBOX_LABEL,
+ PACKAGE_FORWARDING_ENFORCE_LABEL,
+ },
+ components: {
+ GlFormCheckbox,
+ GlFormGroup,
+ GlSprintf,
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ forwarding: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ lockForwarding: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ modelNames: {
+ type: Object,
+ required: true,
+ validator(value) {
+ return isEqual(Object.keys(value), ['forwarding', 'lockForwarding', 'isLocked']);
+ },
+ },
+ },
+ computed: {
+ fields() {
+ return [
+ {
+ testid: 'forwarding-checkbox',
+ label: PACKAGE_FORWARDING_CHECKBOX_LABEL,
+ updateField: this.modelNames.forwarding,
+ checked: this.forwarding,
+ },
+ {
+ testid: 'lock-forwarding-checkbox',
+ label: PACKAGE_FORWARDING_ENFORCE_LABEL,
+ updateField: this.modelNames.lockForwarding,
+ checked: this.lockForwarding,
+ },
+ ];
+ },
+ },
+ methods: {
+ update(type, value) {
+ this.$emit('update', type, value);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group :label="label">
+ <gl-form-checkbox
+ v-for="field in fields"
+ :key="field.testid"
+ :checked="field.checked"
+ :disabled="disabled"
+ :data-testid="field.testid"
+ @change="update(field.updateField, $event)"
+ >
+ <gl-sprintf :message="field.label">
+ <template #packageType>
+ {{ label }}
+ </template>
+ </gl-sprintf>
+ </gl-form-checkbox>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index f285dfc0755..36eb65c623b 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -2,6 +2,7 @@
import { GlAlert } from '@gitlab/ui';
import { n__ } from '~/locale';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
+import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
@@ -11,6 +12,7 @@ export default {
components: {
GlAlert,
PackagesSettings,
+ PackagesForwardingSettings,
DependencyProxySettings,
},
inject: ['groupPath'],
@@ -82,6 +84,12 @@ export default {
@error="handleError(2)"
/>
+ <packages-forwarding-settings
+ :forward-settings="packageSettings"
+ @success="handleSuccess(2)"
+ @error="handleError(2)"
+ />
+
<dependency-proxy-settings
:dependency-proxy-settings="dependencyProxySettings"
:dependency-proxy-image-ttl-policy="dependencyProxyImageTtlPolicy"
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
new file mode 100644
index 00000000000..b7d7f0aaca7
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
@@ -0,0 +1,190 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import {
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ PACKAGE_FORWARDING_FORM_BUTTON,
+ PACKAGE_FORWARDING_FIELDS,
+ MAVEN_FORWARDING_FIELDS,
+} from '~/packages_and_registries/settings/group/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+
+import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import ForwardingSettings from '~/packages_and_registries/settings/group/components/forwarding_settings.vue';
+
+export default {
+ name: 'PackageForwardingSettings',
+ i18n: {
+ PACKAGE_FORWARDING_FORM_BUTTON,
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ },
+ components: {
+ ForwardingSettings,
+ GlButton,
+ SettingsBlock,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['groupPath'],
+ props: {
+ forwardSettings: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ mutationLoading: false,
+ workingCopy: { ...this.forwardSettings },
+ };
+ },
+ computed: {
+ packageForwardingFields() {
+ const fields = PACKAGE_FORWARDING_FIELDS;
+
+ if (this.glFeatures.mavenCentralRequestForwarding) {
+ return fields.concat(MAVEN_FORWARDING_FIELDS);
+ }
+
+ return fields;
+ },
+ isEdited() {
+ return !isEqual(this.forwardSettings, this.workingCopy);
+ },
+ isDisabled() {
+ return !this.isEdited || this.mutationLoading;
+ },
+ npmMutation() {
+ if (this.workingCopy.npmPackageRequestsForwardingLocked) {
+ return {};
+ }
+
+ return {
+ npmPackageRequestsForwarding: this.workingCopy.npmPackageRequestsForwarding,
+ lockNpmPackageRequestsForwarding: this.workingCopy.lockNpmPackageRequestsForwarding,
+ };
+ },
+ pypiMutation() {
+ if (this.workingCopy.pypiPackageRequestsForwardingLocked) {
+ return {};
+ }
+
+ return {
+ pypiPackageRequestsForwarding: this.workingCopy.pypiPackageRequestsForwarding,
+ lockPypiPackageRequestsForwarding: this.workingCopy.lockPypiPackageRequestsForwarding,
+ };
+ },
+ mavenMutation() {
+ if (this.workingCopy.mavenPackageRequestsForwardingLocked) {
+ return {};
+ }
+
+ return {
+ mavenPackageRequestsForwarding: this.workingCopy.mavenPackageRequestsForwarding,
+ lockMavenPackageRequestsForwarding: this.workingCopy.lockMavenPackageRequestsForwarding,
+ };
+ },
+ mutationVariables() {
+ return {
+ ...this.npmMutation,
+ ...this.pypiMutation,
+ ...this.mavenMutation,
+ };
+ },
+ },
+ watch: {
+ forwardSettings(newValue) {
+ this.workingCopy = { ...newValue };
+ },
+ },
+ methods: {
+ isForwardingFieldsDisabled(fields) {
+ const isLocked = fields?.modelNames?.isLocked;
+
+ return this.mutationLoading || this.workingCopy[isLocked];
+ },
+ forwardingFieldsForwarding(fields) {
+ const forwarding = fields?.modelNames?.forwarding;
+
+ return this.workingCopy[forwarding];
+ },
+ forwardingFieldsLockForwarding(fields) {
+ const lockForwarding = fields?.modelNames?.lockForwarding;
+
+ return this.workingCopy[lockForwarding];
+ },
+ async submit() {
+ this.mutationLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: updateNamespacePackageSettings,
+ variables: {
+ input: {
+ namespacePath: this.groupPath,
+ ...this.mutationVariables,
+ },
+ },
+ update: updateGroupPackageSettings(this.groupPath),
+ optimisticResponse: updateGroupPackagesSettingsOptimisticResponse({
+ ...this.forwardSettings,
+ ...this.mutationVariables,
+ }),
+ });
+
+ if (data.updateNamespacePackageSettings?.errors?.length > 0) {
+ throw new Error();
+ } else {
+ this.$emit('success');
+ }
+ } catch {
+ this.$emit('error');
+ } finally {
+ this.mutationLoading = false;
+ }
+ },
+ updateWorkingCopy(type, value) {
+ this.$set(this.workingCopy, type, value);
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block>
+ <template #title> {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_HEADER }}</template>
+ <template #description>
+ <span data-testid="description">
+ {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_DESCRIPTION }}
+ </span>
+ </template>
+ <template #default>
+ <form @submit.prevent="submit">
+ <forwarding-settings
+ v-for="forwardingFields in packageForwardingFields"
+ :key="forwardingFields.label"
+ :data-testid="forwardingFields.testid"
+ :disabled="isForwardingFieldsDisabled(forwardingFields)"
+ :forwarding="forwardingFieldsForwarding(forwardingFields)"
+ :label="forwardingFields.label"
+ :lock-forwarding="forwardingFieldsLockForwarding(forwardingFields)"
+ :model-names="forwardingFields.modelNames"
+ @update="updateWorkingCopy"
+ />
+ <gl-button
+ type="submit"
+ :disabled="isDisabled"
+ :loading="mutationLoading"
+ category="primary"
+ variant="confirm"
+ class="js-no-auto-disable gl-mr-4"
+ >
+ {{ $options.i18n.PACKAGE_FORWARDING_FORM_BUTTON }}
+ </gl-button>
+ </form>
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index 2dd6d3f76f6..c93cd7f7d78 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -7,6 +7,8 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__(
);
export const PACKAGE_FORMATS_TABLE_HEADER = s__('PackageRegistry|Package formats');
export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven');
+export const NPM_PACKAGE_FORMAT = s__('PackageRegistry|npm');
+export const PYPI_PACKAGE_FORMAT = s__('PackageRegistry|PyPI');
export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic');
export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
@@ -15,11 +17,65 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
'PackageRegistry|Publish packages if their name or version matches this regex.',
);
+export const PACKAGE_FORWARDING_SETTINGS_HEADER = s__('PackageRegistry|Package forwarding');
+export const PACKAGE_FORWARDING_SETTINGS_DESCRIPTION = s__(
+ 'PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry.',
+);
+export const PACKAGE_FORWARDING_CHECKBOX_LABEL = s__(
+ `PackageRegistry|Forward %{packageType} package requests`,
+);
+export const PACKAGE_FORWARDING_ENFORCE_LABEL = s__(
+ `PackageRegistry|Enforce %{packageType} setting for all subgroups`,
+);
+
+const MAVEN_PACKAGE_REQUESTS_FORWARDING = 'mavenPackageRequestsForwarding';
+const LOCK_MAVEN_PACKAGE_REQUESTS_FORWARDING = 'lockMavenPackageRequestsForwarding';
+const MAVEN_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'mavenPackageRequestsForwardingLocked';
+const NPM_PACKAGE_REQUESTS_FORWARDING = 'npmPackageRequestsForwarding';
+const LOCK_NPM_PACKAGE_REQUESTS_FORWARDING = 'lockNpmPackageRequestsForwarding';
+const NPM_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'npmPackageRequestsForwardingLocked';
+const PYPI_PACKAGE_REQUESTS_FORWARDING = 'pypiPackageRequestsForwarding';
+const LOCK_PYPI_PACKAGE_REQUESTS_FORWARDING = 'lockPypiPackageRequestsForwarding';
+const PYPI_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'pypiPackageRequestsForwardingLocked';
+
+export const PACKAGE_FORWARDING_FORM_BUTTON = __('Save changes');
+
export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy');
export const DEPENDENCY_PROXY_DESCRIPTION = s__(
'DependencyProxy|Enable the Dependency Proxy and settings for clearing the cache.',
);
+export const PACKAGE_FORWARDING_FIELDS = [
+ {
+ label: NPM_PACKAGE_FORMAT,
+ testid: 'npm',
+ modelNames: {
+ forwarding: NPM_PACKAGE_REQUESTS_FORWARDING,
+ lockForwarding: LOCK_NPM_PACKAGE_REQUESTS_FORWARDING,
+ isLocked: NPM_PACKAGE_REQUESTS_FORWARDING_LOCKED,
+ },
+ },
+ {
+ label: PYPI_PACKAGE_FORMAT,
+ testid: 'pypi',
+ modelNames: {
+ forwarding: PYPI_PACKAGE_REQUESTS_FORWARDING,
+ lockForwarding: LOCK_PYPI_PACKAGE_REQUESTS_FORWARDING,
+ isLocked: PYPI_PACKAGE_REQUESTS_FORWARDING_LOCKED,
+ },
+ },
+];
+
+export const MAVEN_FORWARDING_FIELDS = {
+ label: MAVEN_PACKAGE_FORMAT,
+ testid: 'maven',
+ modelNames: {
+ forwarding: MAVEN_PACKAGE_REQUESTS_FORWARDING,
+ lockForwarding: LOCK_MAVEN_PACKAGE_REQUESTS_FORWARDING,
+ isLocked: MAVEN_PACKAGE_REQUESTS_FORWARDING_LOCKED,
+ },
+};
+
// Parameters
export const PACKAGES_DOCS_PATH = helpPagePath('user/packages/index');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
new file mode 100644
index 00000000000..267e40263f2
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
@@ -0,0 +1,15 @@
+fragment PackageSettingsFields on PackageSettings {
+ mavenDuplicatesAllowed
+ mavenDuplicateExceptionRegex
+ genericDuplicatesAllowed
+ genericDuplicateExceptionRegex
+ mavenPackageRequestsForwarding
+ lockMavenPackageRequestsForwarding
+ mavenPackageRequestsForwardingLocked
+ npmPackageRequestsForwarding
+ lockNpmPackageRequestsForwarding
+ npmPackageRequestsForwardingLocked
+ pypiPackageRequestsForwarding
+ lockPypiPackageRequestsForwarding
+ pypiPackageRequestsForwardingLocked
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
index 5c245ff9453..5558cb66f42 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
@@ -1,10 +1,9 @@
+#import "~/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql"
+
mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsInput!) {
updateNamespacePackageSettings(input: $input) {
packageSettings {
- mavenDuplicatesAllowed
- mavenDuplicateExceptionRegex
- genericDuplicatesAllowed
- genericDuplicateExceptionRegex
+ ...PackageSettingsFields
}
errors
}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_package_forwarding_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_package_forwarding_settings.mutation.graphql
new file mode 100644
index 00000000000..e5e31f03a7d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_package_forwarding_settings.mutation.graphql
@@ -0,0 +1,16 @@
+mutation updatePackageForwardingSettings($input: UpdateNamespacePackageSettingsInput!) {
+ updateNamespacePackageSettings(input: $input) {
+ packageSettings {
+ mavenPackageRequestsForwarding
+ lockMavenPackageRequestsForwarding
+ mavenPackageRequestsForwardingLocked
+ npmPackageRequestsForwarding
+ lockNpmPackageRequestsForwarding
+ npmPackageRequestsForwardingLocked
+ pypiPackageRequestsForwarding
+ lockPypiPackageRequestsForwarding
+ pypiPackageRequestsForwardingLocked
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
index 404d9d26d49..82a282d6d81 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
@@ -1,3 +1,5 @@
+#import "~/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql"
+
query getGroupPackagesSettings($fullPath: ID!) {
group(fullPath: $fullPath) {
id
@@ -9,10 +11,7 @@ query getGroupPackagesSettings($fullPath: ID!) {
enabled
}
packageSettings {
- mavenDuplicatesAllowed
- mavenDuplicateExceptionRegex
- genericDuplicatesAllowed
- genericDuplicateExceptionRegex
+ ...PackageSettingsFields
}
}
}
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/delete_package_modal.vue b/app/assets/javascripts/packages_and_registries/shared/components/delete_package_modal.vue
new file mode 100644
index 00000000000..b66b0b3548d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/shared/components/delete_package_modal.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { __ } from '~/locale';
+import {
+ DELETE_PACKAGE_MODAL_CONTENT_MESSAGE,
+ DELETE_PACKAGE_MODAL_TITLE,
+ DELETE_PACKAGE_MODAL_ACTION,
+} from '~/packages_and_registries/shared/constants';
+import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants';
+
+export default {
+ components: {
+ GlModal,
+ GlSprintf,
+ },
+ props: {
+ itemToBeDeleted: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ isModalVisible() {
+ return Boolean(this.itemToBeDeleted);
+ },
+ deletePackageName() {
+ return this.itemToBeDeleted?.name ?? '';
+ },
+ deleteModalActionPrimaryProps() {
+ return {
+ text: this.$options.i18n.modalAction,
+ attributes: {
+ variant: 'danger',
+ },
+ };
+ },
+ deleteModalActionCancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
+ tracking() {
+ return {
+ category: TRACK_CATEGORY,
+ };
+ },
+ },
+ methods: {
+ deleteItemConfirmation() {
+ this.$emit('ok');
+ },
+ onChangeModalVisibility(isVisible) {
+ if (!isVisible) this.$emit('cancel');
+ },
+ },
+ i18n: {
+ modalTitle: DELETE_PACKAGE_MODAL_TITLE,
+ modalDescription: DELETE_PACKAGE_MODAL_CONTENT_MESSAGE,
+ modalAction: DELETE_PACKAGE_MODAL_ACTION,
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :visible="isModalVisible"
+ size="sm"
+ modal-id="confirm-delete-package"
+ :title="$options.i18n.modalTitle"
+ :action-primary="deleteModalActionPrimaryProps"
+ :action-cancel="deleteModalActionCancelProps"
+ @ok="deleteItemConfirmation"
+ @change="onChangeModalVisibility"
+ >
+ <template #modal-title>{{ $options.i18n.modalTitle }}</template>
+ <gl-sprintf :message="$options.i18n.modalDescription">
+ <template #name>
+ <strong>{{ deletePackageName }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
index 7fd440d0b27..f3ce967b756 100644
--- a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
+++ b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
@@ -39,6 +39,12 @@ export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
'PackageRegistry|Package asset deleted successfully',
);
+export const DELETE_PACKAGE_MODAL_CONTENT_MESSAGE = s__(
+ 'PackageRegistry|You are about to delete %{name}, are you sure?',
+);
+export const DELETE_PACKAGE_MODAL_TITLE = s__('PackageRegistry|Delete package');
+export const DELETE_PACKAGE_MODAL_ACTION = s__('PackageRegistry|Permanently delete');
+
export const PACKAGE_ERROR_STATUS = 'error';
export const PACKAGE_DEFAULT_STATUS = 'default';
export const PACKAGE_HIDDEN_STATUS = 'hidden';
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index ccb449f96e1..b68148e5461 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -44,6 +44,7 @@ export default {
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
'sendUserConfirmationEmail',
+ 'emailConfirmationSetting',
'minimumPasswordLength',
'minimumPasswordLengthMin',
'minimumPasswordLengthMax',
@@ -58,6 +59,8 @@ export default {
'emailRestrictions',
'afterSignUpText',
'pendingUserCount',
+ 'projectSharingHelpLink',
+ 'groupSharingHelpLink',
],
data() {
return {
@@ -66,6 +69,7 @@ export default {
signupEnabled: this.signupEnabled,
requireAdminApproval: this.requireAdminApprovalAfterUserSignup,
sendConfirmationEmail: this.sendUserConfirmationEmail,
+ emailConfirmationSetting: this.emailConfirmationSetting,
minimumPasswordLength: this.minimumPasswordLength,
minimumPasswordLengthMin: this.minimumPasswordLengthMin,
minimumPasswordLengthMax: this.minimumPasswordLengthMax,
@@ -81,6 +85,8 @@ export default {
supportedSyntaxLinkUrl: this.supportedSyntaxLinkUrl,
emailRestrictions: this.emailRestrictions,
afterSignUpText: this.afterSignUpText,
+ projectSharingHelpLink: this.projectSharingHelpLink,
+ groupSharingHelpLink: this.groupSharingHelpLink,
},
};
},
@@ -199,6 +205,15 @@ export default {
signupEnabledLabel: s__('ApplicationSettings|Sign-up enabled'),
requireAdminApprovalLabel: s__('ApplicationSettings|Require admin approval for new sign-ups'),
sendConfirmationEmailLabel: s__('ApplicationSettings|Send confirmation email on sign-up'),
+ emailConfirmationSettingsLabel: s__('ApplicationSettings|Email confirmation settings'),
+ emailConfirmationSettingsOffLabel: s__('ApplicationSettings|Off'),
+ emailConfirmationSettingsOffHelpText: s__(
+ 'ApplicationSettings|New users can sign up without confirming their email address.',
+ ),
+ emailConfirmationSettingsHardLabel: s__('ApplicationSettings|Hard'),
+ emailConfirmationSettingsHardHelpText: s__(
+ 'ApplicationSettings|Send a confirmation email during sign up. New users must confirm their email address before they can log in.',
+ ),
minimumPasswordLengthLabel: s__(
'ApplicationSettings|Minimum password length (number of characters)',
),
@@ -208,7 +223,7 @@ export default {
),
userCapLabel: s__('ApplicationSettings|User cap'),
userCapDescription: s__(
- 'ApplicationSettings|After the instance reaches the user cap, any user who is added or requests access must be approved by an administrator. Leave blank for unlimited.',
+ 'ApplicationSettings|After the instance reaches the user cap, any user who is added or requests access must be approved by an administrator. Leave blank for an unlimited user cap. If you change the user cap to unlimited, you must re-enable %{projectSharingLinkStart}project sharing%{projectSharingLinkEnd} and %{groupSharingLinkStart}group sharing%{groupSharingLinkEnd}.',
),
domainDenyListGroupLabel: s__('ApplicationSettings|Domain denylist'),
domainDenyListLabel: s__('ApplicationSettings|Enable domain denylist for sign-ups'),
@@ -276,9 +291,28 @@ export default {
:label="$options.i18n.sendConfirmationEmailLabel"
/>
+ <gl-form-group :label="$options.i18n.emailConfirmationSettingsLabel">
+ <gl-form-radio-group
+ v-model="form.emailConfirmationSetting"
+ name="application_setting[email_confirmation_setting]"
+ >
+ <gl-form-radio value="hard">
+ {{ $options.i18n.emailConfirmationSettingsHardLabel }}
+
+ <template #help> {{ $options.i18n.emailConfirmationSettingsHardHelpText }} </template>
+ </gl-form-radio>
+ <gl-form-radio value="off">
+ {{ $options.i18n.emailConfirmationSettingsOffLabel }}
+
+ <template #help> {{ $options.i18n.emailConfirmationSettingsOffHelpText }} </template>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ </gl-form-group>
+
<gl-form-group
:label="$options.i18n.userCapLabel"
:description="$options.i18n.userCapDescription"
+ data-testid="user-cap-form-group"
>
<gl-form-input
v-model="form.userCap"
@@ -286,6 +320,17 @@ export default {
name="application_setting[new_user_signups_cap]"
data-testid="user-cap-input"
/>
+
+ <template #description>
+ <gl-sprintf :message="$options.i18n.userCapDescription">
+ <template #projectSharingLink="{ content }">
+ <gl-link :href="projectSharingHelpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ <template #groupSharingLink="{ content }">
+ <gl-link :href="groupSharingHelpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
</gl-form-group>
<gl-form-group :label="$options.i18n.minimumPasswordLengthLabel">
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/constants.js b/app/assets/javascripts/pages/admin/jobs/index/components/constants.js
new file mode 100644
index 00000000000..9e2d464bc4d
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/constants.js
@@ -0,0 +1,11 @@
+import { s__, __ } from '~/locale';
+
+export const STOP_JOBS_MODAL_ID = 'stop-jobs-modal';
+export const STOP_JOBS_MODAL_TITLE = s__('AdminArea|Stop all jobs?');
+export const STOP_JOBS_BUTTON_TEXT = s__('AdminArea|Stop all jobs');
+export const CANCEL_TEXT = __('Cancel');
+export const STOP_JOBS_FAILED_TEXT = s__('AdminArea|Stopping jobs failed');
+export const PRIMARY_ACTION_TEXT = s__('AdminArea|Stop jobs');
+export const STOP_JOBS_WARNING = s__(
+ 'AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.',
+);
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index 4f42ef2892d..b608b3b9492 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -3,7 +3,14 @@ import { GlModal } from '@gitlab/ui';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
-import { __, s__ } from '~/locale';
+import {
+ CANCEL_TEXT,
+ STOP_JOBS_MODAL_ID,
+ STOP_JOBS_FAILED_TEXT,
+ STOP_JOBS_MODAL_TITLE,
+ STOP_JOBS_WARNING,
+ PRIMARY_ACTION_TEXT,
+} from './constants';
export default {
components: {
@@ -15,13 +22,6 @@ export default {
required: true,
},
},
- computed: {
- text() {
- return s__(
- 'AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.',
- );
- },
- },
methods: {
onSubmit() {
return axios
@@ -32,30 +32,33 @@ export default {
})
.catch((error) => {
createAlert({
- message: s__('AdminArea|Stopping jobs failed'),
+ message: STOP_JOBS_FAILED_TEXT,
});
throw error;
});
},
},
primaryAction: {
- text: s__('AdminArea|Stop jobs'),
+ text: PRIMARY_ACTION_TEXT,
attributes: [{ variant: 'danger' }],
},
cancelAction: {
- text: __('Cancel'),
+ text: CANCEL_TEXT,
},
+ STOP_JOBS_WARNING,
+ STOP_JOBS_MODAL_ID,
+ STOP_JOBS_MODAL_TITLE,
};
</script>
<template>
<gl-modal
- modal-id="stop-jobs-modal"
+ :modal-id="$options.STOP_JOBS_MODAL_ID"
:action-primary="$options.primaryAction"
:action-cancel="$options.cancelAction"
@primary="onSubmit"
>
- <template #modal-title>{{ s__('AdminArea|Stop all jobs?') }}</template>
- {{ text }}
+ <template #modal-title>{{ $options.STOP_JOBS_MODAL_TITLE }}</template>
+ {{ $options.STOP_JOBS_WARNING }}
</gl-modal>
</template>
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 4cd312b403c..c82b186f671 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -1,29 +1,29 @@
import Vue from 'vue';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
+import { STOP_JOBS_MODAL_ID } from './components/constants';
import StopJobsModal from './components/stop_jobs_modal.vue';
Vue.use(Translate);
function initJobs() {
const buttonId = 'js-stop-jobs-button';
- const modalId = 'stop-jobs-modal';
const stopJobsButton = document.getElementById(buttonId);
if (stopJobsButton) {
// eslint-disable-next-line no-new
new Vue({
- el: `#js-${modalId}`,
+ el: `#js-${STOP_JOBS_MODAL_ID}`,
components: {
StopJobsModal,
},
mounted() {
stopJobsButton.classList.remove('disabled');
stopJobsButton.addEventListener('click', () => {
- this.$root.$emit(BV_SHOW_MODAL, modalId, `#${buttonId}`);
+ this.$root.$emit(BV_SHOW_MODAL, STOP_JOBS_MODAL_ID, `#${buttonId}`);
});
},
render(createElement) {
- return createElement(modalId, {
+ return createElement(STOP_JOBS_MODAL_ID, {
props: {
url: stopJobsButton.dataset.url,
},
diff --git a/app/assets/javascripts/pages/admin/runners/edit/index.js b/app/assets/javascripts/pages/admin/runners/edit/index.js
index 03d31f49a99..dcce2a8dafd 100644
--- a/app/assets/javascripts/pages/admin/runners/edit/index.js
+++ b/app/assets/javascripts/pages/admin/runners/edit/index.js
@@ -1,3 +1,3 @@
-import { initRunnerEdit } from '~/runner/runner_edit';
+import { initRunnerEdit } from '~/ci/runner/runner_edit';
initRunnerEdit('#js-admin-runner-edit');
diff --git a/app/assets/javascripts/pages/admin/runners/index/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js
index f83111b6385..bc7b1421b31 100644
--- a/app/assets/javascripts/pages/admin/runners/index/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index/index.js
@@ -1,3 +1,3 @@
-import { initAdminRunners } from '~/runner/admin_runners';
+import { initAdminRunners } from '~/ci/runner/admin_runners';
initAdminRunners();
diff --git a/app/assets/javascripts/pages/admin/runners/show/index.js b/app/assets/javascripts/pages/admin/runners/show/index.js
index f76f3a2430d..fd38abf221a 100644
--- a/app/assets/javascripts/pages/admin/runners/show/index.js
+++ b/app/assets/javascripts/pages/admin/runners/show/index.js
@@ -1,3 +1,3 @@
-import { initAdminRunnerShow } from '~/runner/admin_runner_show';
+import { initAdminRunnerShow } from '~/ci/runner/admin_runner_show';
initAdminRunnerShow();
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index d0903ad53bc..08c247a498b 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -1,4 +1,5 @@
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { mountIssuesDashboardApp } from '~/issues/dashboard';
import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
@@ -12,3 +13,5 @@ initFilteredSearch({
projectSelect();
initManualOrdering();
+
+mountIssuesDashboardApp();
diff --git a/app/assets/javascripts/pages/groups/observability/dashboards/index.js b/app/assets/javascripts/pages/groups/observability/dashboards/index.js
new file mode 100644
index 00000000000..c3b6ce6f99f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/observability/dashboards/index.js
@@ -0,0 +1,3 @@
+import ObservabilityApp from '~/observability';
+
+ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/explore/index.js b/app/assets/javascripts/pages/groups/observability/explore/index.js
new file mode 100644
index 00000000000..c3b6ce6f99f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/observability/explore/index.js
@@ -0,0 +1,3 @@
+import ObservabilityApp from '~/observability';
+
+ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/observability/manage/index.js b/app/assets/javascripts/pages/groups/observability/manage/index.js
new file mode 100644
index 00000000000..c3b6ce6f99f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/observability/manage/index.js
@@ -0,0 +1,3 @@
+import ObservabilityApp from '~/observability';
+
+ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/runners/edit/index.js b/app/assets/javascripts/pages/groups/runners/edit/index.js
index febb0026b67..08ae57866e2 100644
--- a/app/assets/javascripts/pages/groups/runners/edit/index.js
+++ b/app/assets/javascripts/pages/groups/runners/edit/index.js
@@ -1,3 +1,3 @@
-import { initRunnerEdit } from '~/runner/runner_edit';
+import { initRunnerEdit } from '~/ci/runner/runner_edit';
initRunnerEdit('#js-group-runner-edit');
diff --git a/app/assets/javascripts/pages/groups/runners/index/index.js b/app/assets/javascripts/pages/groups/runners/index/index.js
index ca1a6bdab75..1cc07d1069a 100644
--- a/app/assets/javascripts/pages/groups/runners/index/index.js
+++ b/app/assets/javascripts/pages/groups/runners/index/index.js
@@ -1,3 +1,3 @@
-import { initGroupRunners } from '~/runner/group_runners';
+import { initGroupRunners } from '~/ci/runner/group_runners';
initGroupRunners();
diff --git a/app/assets/javascripts/pages/groups/runners/show/index.js b/app/assets/javascripts/pages/groups/runners/show/index.js
index c59e3b80dc1..755e38004ad 100644
--- a/app/assets/javascripts/pages/groups/runners/show/index.js
+++ b/app/assets/javascripts/pages/groups/runners/show/index.js
@@ -1,3 +1,3 @@
-import { initGroupRunnerShow } from '~/runner/group_runner_show';
+import { initGroupRunnerShow } from '~/ci/runner/group_runner_show';
initGroupRunnerShow();
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 2aec0617b5a..52124865bcc 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -1,23 +1,12 @@
/* eslint-disable no-new */
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
-import { getPagePath, getDashPath } from '~/lib/utils/common_utils';
import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
-import GroupTabs from './group_tabs';
-export default function initGroupDetails(actionName = 'show') {
- const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED];
- const dashPath = getDashPath();
- let action = loadableActions.includes(dashPath) ? dashPath : getPagePath(1);
- if (actionName && action === actionName) {
- action = 'show'; // 'show' resets GroupTabs to default action through base class
- }
-
- new GroupTabs({ parentEl: '.groups-listing', action });
+export default function initGroupDetails() {
new ShortcutsNavigation();
initNotificationsDropdown();
diff --git a/app/assets/javascripts/pages/groups/shared/group_tabs.js b/app/assets/javascripts/pages/groups/shared/group_tabs.js
deleted file mode 100644
index 73d810007dc..00000000000
--- a/app/assets/javascripts/pages/groups/shared/group_tabs.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import $ from 'jquery';
-import createGroupTree from '~/groups';
-import {
- ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- ACTIVE_TAB_SHARED,
- ACTIVE_TAB_ARCHIVED,
- CONTENT_LIST_CLASS,
- GROUPS_LIST_HOLDER_CLASS,
- GROUPS_FILTER_FORM_CLASS,
-} from '~/groups/constants';
-import GroupFilterableList from '~/groups/groups_filterable_list';
-import { removeParams } from '~/lib/utils/url_utility';
-import UserTabs from '~/pages/users/user_tabs';
-
-export default class GroupTabs extends UserTabs {
- constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) {
- super({ defaultAction, action, parentEl });
- }
-
- bindEvents() {
- this.$parentEl
- .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
- .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', (event) => this.tabShown(event));
- }
-
- tabShown(event) {
- const $target = $(event.target);
- const action = $target.data('action') || $target.data('targetSection');
- const source = $target.attr('href') || $target.data('targetPath');
-
- document.querySelector(GROUPS_FILTER_FORM_CLASS).action = source;
-
- this.setTab(action);
- return this.setCurrentAction(source);
- }
-
- setTab(action) {
- const loadableActions = [
- ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- ACTIVE_TAB_SHARED,
- ACTIVE_TAB_ARCHIVED,
- ];
- this.enableSearchBar(action);
- this.action = action;
-
- if (this.loaded[action]) {
- return;
- }
-
- if (loadableActions.includes(action)) {
- this.cleanFilterState();
- this.loadTab(action);
- }
- }
-
- loadTab(action) {
- const elId = `js-groups-${action}-tree`;
- const endpoint = this.getEndpoint(action);
-
- this.toggleLoading(true);
-
- createGroupTree(elId, endpoint, action);
- this.loaded[action] = true;
-
- this.toggleLoading(false);
- }
-
- getEndpoint(action) {
- const { endpointsDefault, endpointsShared } = this.$parentEl.data();
- let endpoint;
-
- switch (action) {
- case ACTIVE_TAB_ARCHIVED:
- endpoint = `${endpointsDefault}?archived=only`;
- break;
- case ACTIVE_TAB_SHARED:
- endpoint = endpointsShared;
- break;
- default:
- // ACTIVE_TAB_SUBGROUPS_AND_PROJECTS
- endpoint = endpointsDefault;
- break;
- }
-
- return endpoint;
- }
-
- enableSearchBar(action) {
- const containerEl = document.getElementById(action);
- const form = document.querySelector(GROUPS_FILTER_FORM_CLASS);
- const filter = form.querySelector('.js-groups-list-filter');
- const holder = containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS);
- const dataEl = containerEl.querySelector(CONTENT_LIST_CLASS);
- const endpoint = this.getEndpoint(action);
-
- if (!dataEl) {
- return;
- }
-
- const { dataset } = dataEl;
- const opts = {
- form,
- filter,
- holder,
- filterEndpoint: endpoint || dataset.endpoint,
- pagePath: null,
- dropdownSel: '.js-group-filter-dropdown-wrap',
- filterInputField: 'filter',
- action,
- };
-
- if (!this.loaded[action]) {
- const filterableList = new GroupFilterableList(opts);
- filterableList.initSearch();
- }
- }
-
- cleanFilterState() {
- const values = Object.values(this.loaded);
- const loadedTabs = values.filter((e) => e === true);
-
- if (!loadedTabs.length) {
- return;
- }
-
- const newState = removeParams(['page'], window.location.search);
-
- window.history.replaceState(
- {
- url: newState,
- },
- document.title,
- newState,
- );
- }
-}
diff --git a/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js b/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
index 80b911493a8..91d5013c67d 100644
--- a/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
+++ b/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
@@ -8,7 +8,7 @@ export const initTimezoneDropdown = () => {
return null;
}
- const { timezoneData, initialValue } = el.dataset;
+ const { timezoneData, initialValue, name } = el.dataset;
const timezones = JSON.parse(timezoneData);
const timezoneDropdown = new Vue({
@@ -23,7 +23,7 @@ export const initTimezoneDropdown = () => {
props: {
value: this.value,
timezoneData: timezones,
- name: 'user[timezone]',
+ name,
},
class: 'gl-md-form-input-lg',
});
diff --git a/app/assets/javascripts/pages/projects/artifacts/index.js b/app/assets/javascripts/pages/projects/artifacts/index.js
new file mode 100644
index 00000000000..4aa9b225790
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/artifacts/index.js
@@ -0,0 +1,3 @@
+import { initArtifactsTable } from '~/artifacts/index';
+
+initArtifactsTable();
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index f3530b46845..ac5e0b28dd1 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -3,6 +3,7 @@ import BranchSortDropdown from '~/branches/branch_sort_dropdown';
import initDiverganceGraph from '~/branches/divergence_graph';
import initDeleteBranchButton from '~/branches/init_delete_branch_button';
import initDeleteBranchModal from '~/branches/init_delete_branch_modal';
+import initDeleteMergedBranches from '~/branches/init_delete_merged_branches';
const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
'.js-branch-list',
@@ -11,6 +12,7 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
BranchSortDropdown();
initDeprecatedRemoveRowBehavior();
+initDeleteMergedBranches();
document
.querySelectorAll('.js-delete-branch-button')
diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js
index ed476d25f8b..9e559354205 100644
--- a/app/assets/javascripts/pages/projects/hooks/index.js
+++ b/app/assets/javascripts/pages/projects/hooks/index.js
@@ -1,5 +1,7 @@
import initSearchSettings from '~/search_settings';
import initWebhookForm from '~/webhooks';
+import { initPushEventsEditForm } from '~/webhooks/webhook';
initSearchSettings();
initWebhookForm();
+initPushEventsEditForm();
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index f37a2987685..097b2f33aa9 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -3,7 +3,6 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import LineHighlighter from '~/blob/line_highlighter';
-import initBlobBundle from '~/blob_edit/blob_bundle';
export default () => {
new LineHighlighter(); // eslint-disable-line no-new
@@ -35,6 +34,4 @@ export default () => {
suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
}).init();
-
- initBlobBundle();
};
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 4eab0cccb06..3717d8027c4 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -86,6 +86,7 @@ export default {
:target="openInNewTab ? '_blank' : '_self'"
:href="value.url"
data-testid="uncompleted-learn-gitlab-link"
+ data-qa-selector="uncompleted_learn_gitlab_link"
data-track-action="click_link"
:data-track-label="actionLabelValue('trackLabel')"
>{{ actionLabelValue('title') }}</gl-link
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index ebf7c266482..42fa306d226 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -6,8 +6,8 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import GLForm from '~/gl_form';
import LabelsSelect from '~/labels/labels_select';
-import MilestoneSelect from '~/milestones/milestone_select';
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
+import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
export default () => {
new Diff();
@@ -15,8 +15,8 @@ export default () => {
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
- new MilestoneSelect();
new IssuableTemplateSelectors({
warnTemplateOverride: true,
});
+ mountMilestoneDropdown('[name="merge_request[milestone_id]"]');
};
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
new file mode 100644
index 00000000000..0a9d9f4c987
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+
+const initShowExperiment = () => {
+ const element = document.querySelector('#js-show-ml-experiment');
+ if (!element) {
+ return;
+ }
+
+ const container = document.createElement('div');
+ element.appendChild(container);
+
+ const candidates = JSON.parse(element.dataset.candidates);
+ const metricNames = JSON.parse(element.dataset.metrics);
+ const paramNames = JSON.parse(element.dataset.params);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: container,
+ provide: {
+ candidates,
+ metricNames,
+ paramNames,
+ },
+ render(h) {
+ return h(ShowExperiment);
+ },
+ });
+};
+
+initShowExperiment();
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 0edce2db0a3..e2a782bc5d8 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -1,4 +1,4 @@
-import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app';
+import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
if (gon.features?.pipelineSchedulesVue) {
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 7d0930f6424..27610df482d 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
-import initPipelineSchedulesApp from '~/pipeline_schedules/mount_pipeline_schedules_app';
-import PipelineSchedulesTakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue';
+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() {
@@ -58,7 +58,7 @@ function initTakeownershipModal() {
});
},
render(createElement) {
- return createElement(PipelineSchedulesTakeOwnershipModal, {
+ return createElement(PipelineSchedulesTakeOwnershipModalLegacy, {
props: {
ownershipUrl: this.url,
},
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 06084fa729b..d8ba7bbd752 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -1,4 +1,4 @@
-import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app';
+import initPipelineSchedulesFormApp from '~/ci/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
if (gon.features?.pipelineSchedulesVue) {
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
index eae721771de..abd17efc498 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js
@@ -6,8 +6,8 @@ import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import setupNativeFormVariableList from '~/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';
-import TimezoneDropdown from './components/timezone_dropdown';
Vue.use(Translate);
@@ -81,13 +81,6 @@ export default () => {
const formElement = document.getElementById('new-pipeline-schedule-form');
- gl.timezoneDropdown = new TimezoneDropdown({
- $dropdownEl: $('.js-timezone-dropdown'),
- $inputEl: $('#schedule_cron_timezone'),
- onSelectTimezone: () => {
- gl.pipelineScheduleFieldErrors.updateFormValidityState();
- },
- });
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
initTargetRefDropdown();
@@ -97,3 +90,5 @@ export default () => {
formField: 'schedule',
});
};
+
+initTimezoneDropdown();
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 3e5c02bbf19..c37b4cc643a 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -41,6 +41,8 @@ export default {
featureFlagsHelpText: s__(
'ProjectSettings|Roll out new features without redeploying with feature flags.',
),
+ infrastructureLabel: s__('ProjectSettings|Infrastructure'),
+ infrastructureHelpText: s__('ProjectSettings|Configure your infrastructure.'),
monitorLabel: s__('ProjectSettings|Monitor'),
packagesHelpText: s__(
'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.',
@@ -157,6 +159,11 @@ export default {
required: false,
default: '',
},
+ infrastructureHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
releasesHelpPath: {
type: String,
required: false,
@@ -245,6 +252,7 @@ export default {
operationsAccessLevel: featureAccessLevel.EVERYONE,
environmentsAccessLevel: featureAccessLevel.EVERYONE,
featureFlagsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
+ infrastructureAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
releasesAccessLevel: featureAccessLevel.EVERYONE,
monitorAccessLevel: featureAccessLevel.EVERYONE,
containerRegistryAccessLevel: featureAccessLevel.EVERYONE,
@@ -433,6 +441,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.featureFlagsAccessLevel,
);
+ this.infrastructureAccessLevel = Math.min(
+ featureAccessLevel.PROJECT_MEMBERS,
+ this.infrastructureAccessLevel,
+ );
this.releasesAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.releasesAccessLevel,
@@ -981,6 +993,19 @@ export default {
name="project[project_feature_attributes][feature_flags_access_level]"
/>
</project-setting-row>
+ <project-setting-row
+ ref="infrastructure-settings"
+ :label="$options.i18n.infrastructureLabel"
+ :help-text="$options.i18n.infrastructureHelpText"
+ :help-path="infrastructureHelpPath"
+ >
+ <project-feature-setting
+ v-model="infrastructureAccessLevel"
+ :label="$options.i18n.infrastructureLabel"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][infrastructure_access_level]"
+ />
+ </project-setting-row>
</template>
<project-setting-row
ref="releases-settings"
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index eff39a744ad..1de36f4a0fb 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -10,14 +10,6 @@ import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
// Project show page loads different overview content based on user preferences
-if (document.querySelector('.js-upload-blob-form')) {
- import(/* webpackChunkName: 'blobBundle' */ '~/blob_edit/blob_bundle')
- .then(({ initUploadForm }) => {
- initUploadForm();
- })
- .catch(() => {});
-}
-
if (document.getElementById('js-tree-list')) {
import(/* webpackChunkName: 'treeList' */ 'ee_else_ce/repository')
.then(({ default: initTree }) => {
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index cf7162f477d..17c17014ece 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -1,10 +1,8 @@
import $ from 'jquery';
import initTree from 'ee_else_ce/repository';
-import initBlob from '~/blob_edit/blob_bundle';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import NewCommitForm from '~/new_commit_form';
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
-initBlob();
initTree();
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 7b9656de362..8e2f542aec0 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -343,7 +343,7 @@ export default {
:uploads-path="pageInfo.uploadsPath"
:enable-content-editor="isMarkdownFormat"
:enable-preview="isMarkdownFormat"
- :init-on-autofocus="pageInfo.persisted"
+ :autofocus="pageInfo.persisted"
:form-field-placeholder="$options.i18n.content.placeholder"
:form-field-aria-label="$options.i18n.content.label"
form-field-id="wiki_content"
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 2580cbcb944..139da5dabbd 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -23,9 +23,9 @@ const PERSISTENT_USER_CALLOUTS = [
];
const initCallouts = () => {
- PERSISTENT_USER_CALLOUTS.forEach((calloutContainer) =>
- PersistentUserCallout.factory(document.querySelector(calloutContainer)),
- );
+ document
+ .querySelectorAll(PERSISTENT_USER_CALLOUTS)
+ .forEach((calloutContainer) => PersistentUserCallout.factory(calloutContainer));
};
export default initCallouts;
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index 4941f22230b..ed5466ff99c 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -219,7 +219,6 @@ export default {
:empty-message="$options.i18n.empty.merge"
:keep-component-mounted="false"
:is-empty="isEmpty"
- :is-invalid="isInvalid"
:is-unavailable="isLintUnavailable"
:title="$options.i18n.tabMergedYaml"
lazy
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue
deleted file mode 100644
index 4a08a82275a..00000000000
--- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue
+++ /dev/null
@@ -1,134 +0,0 @@
-<script>
-import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
-import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline_schedule.mutation.graphql';
-import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
-import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
-
-export default {
- i18n: {
- schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
- scheduleDeleteError: s__(
- 'PipelineSchedules|There was a problem deleting the pipeline schedule.',
- ),
- },
- modal: {
- id: 'delete-pipeline-schedule-modal',
- deleteConfirmation: s__(
- 'PipelineSchedules|Are you sure you want to delete this pipeline schedule?',
- ),
- actionPrimary: {
- text: s__('PipelineSchedules|Delete pipeline schedule'),
- attributes: [{ variant: 'danger' }],
- },
- actionCancel: {
- text: __('Cancel'),
- attributes: [],
- },
- },
- components: {
- GlAlert,
- GlLoadingIcon,
- GlModal,
- PipelineSchedulesTable,
- },
- inject: {
- fullPath: {
- default: '',
- },
- },
- apollo: {
- schedules: {
- query: getPipelineSchedulesQuery,
- variables() {
- return {
- projectPath: this.fullPath,
- };
- },
- update({ project }) {
- return project?.pipelineSchedules?.nodes || [];
- },
- error() {
- this.reportError(this.$options.i18n.schedulesFetchError);
- },
- },
- },
- data() {
- return {
- schedules: [],
- hasError: false,
- errorMessage: '',
- scheduleToDeleteId: null,
- showModal: false,
- };
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.schedules.loading;
- },
- },
- methods: {
- reportError(error) {
- this.hasError = true;
- this.errorMessage = error;
- },
- showDeleteModal(id) {
- this.showModal = true;
- this.scheduleToDeleteId = id;
- },
- hideModal() {
- this.showModal = false;
- this.scheduleToDeleteId = null;
- },
- async deleteSchedule() {
- try {
- const {
- data: {
- pipelineScheduleDelete: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: deletePipelineScheduleMutation,
- variables: { id: this.scheduleToDeleteId },
- });
-
- if (errors.length > 0) {
- throw new Error();
- } else {
- this.$apollo.queries.schedules.refetch();
- }
- } catch {
- this.reportError(this.$options.i18n.scheduleDeleteError);
- }
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-alert v-if="hasError" class="gl-mb-2" variant="danger" @dismiss="hasError = false">
- {{ errorMessage }}
- </gl-alert>
-
- <gl-loading-icon v-if="isLoading" size="lg" />
-
- <!-- Tabs will be addressed in #371989 -->
-
- <template v-else>
- <pipeline-schedules-table :schedules="schedules" @showDeleteModal="showDeleteModal" />
-
- <gl-modal
- :visible="showModal"
- :title="$options.modal.actionPrimary.text"
- :modal-id="$options.modal.id"
- :action-primary="$options.modal.actionPrimary"
- :action-cancel="$options.modal.actionCancel"
- size="sm"
- @primary="deleteSchedule"
- @hide="hideModal"
- >
- {{ $options.modal.deleteConfirmation }}
- </gl-modal>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 475dd3bf36e..be12df68f76 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -29,7 +29,7 @@ export default {
dagDocPath: {
default: null,
},
- emptySvgPath: {
+ emptyDagSvgPath: {
default: '',
},
pipelineIid: {
@@ -213,7 +213,7 @@ export default {
/>
<gl-empty-state
v-else-if="hasNoDependentJobs"
- :svg-path="emptySvgPath"
+ :svg-path="emptyDagSvgPath"
:title="$options.emptyStateTexts.title"
>
<template #description>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
index f1c6c6633eb..ba150919e58 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
@@ -27,6 +27,7 @@ export default {
},
dropdownPopperOpts: {
placement: 'bottom',
+ positionFixed: true,
},
components: {
CiIcon,
@@ -136,25 +137,19 @@ export default {
:is-active="isDropdownOpen"
:size="24"
:status="stage.status"
- class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
+ class="gl-display-inline-flex gl-align-items-center gl-border gl-z-index-1"
/>
</template>
- <div
- v-if="isLoading"
- class="gl-display-flex gl-justify-content-center gl-p-2"
- data-testid="pipeline-stage-loading-state"
- >
+ <div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
<gl-loading-icon size="sm" class="gl-mr-3" />
- <p class="gl-mb-0">{{ $options.i18n.loadingText }}</p>
+ <p class="gl-line-height-normal gl-mb-0">{{ $options.i18n.loadingText }}</p>
</div>
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
- <div
- class="gl-align-items-center gl-border-b gl-display-flex gl-font-weight-bold gl-justify-content-center gl-pb-3"
- >
+ <div class="gl--flex-center gl-border-b gl-font-weight-bold gl-pb-3">
<span class="gl-mr-1">{{ $options.i18n.stage }}</span>
<span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
index 2a78636261b..3798863ae60 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
@@ -1,12 +1,13 @@
<script>
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
import { __ } from '~/locale';
-import { failedJobsTabName, jobsTabName, needsTabName, testReportTabName } from '../constants';
-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 {
+ failedJobsTabName,
+ jobsTabName,
+ needsTabName,
+ pipelineTabName,
+ testReportTabName,
+} from '../constants';
export default {
i18n: {
@@ -19,20 +20,16 @@ export default {
},
},
tabNames: {
+ pipeline: pipelineTabName,
needs: needsTabName,
jobs: jobsTabName,
failures: failedJobsTabName,
tests: testReportTabName,
},
components: {
- Dag,
GlBadge,
GlTab,
GlTabs,
- JobsApp,
- FailedJobsApp,
- PipelineGraphWrapper,
- TestReports,
},
inject: [
'defaultTabValue',
@@ -41,14 +38,27 @@ export default {
'totalJobCount',
'testsCount',
],
+ data() {
+ return {
+ activeTab: this.defaultTabValue,
+ };
+ },
computed: {
showFailedJobsTab() {
return this.failedJobsCount > 0;
},
},
+ watch: {
+ $route(to) {
+ this.activeTab = to.name;
+ },
+ },
methods: {
isActive(tabName) {
- return tabName === this.defaultTabValue;
+ return tabName === this.activeTab;
+ },
+ navigateTo(tabName) {
+ this.$router.push({ name: tabName });
},
},
};
@@ -59,10 +69,12 @@ export default {
<gl-tab
ref="pipelineTab"
:title="$options.i18n.tabs.pipelineTitle"
+ :active="isActive($options.tabNames.pipeline)"
data-testid="pipeline-tab"
lazy
+ @click="navigateTo($options.tabNames.pipeline)"
>
- <pipeline-graph-wrapper />
+ <router-view />
</gl-tab>
<gl-tab
ref="dagTab"
@@ -70,15 +82,21 @@ export default {
:active="isActive($options.tabNames.needs)"
data-testid="dag-tab"
lazy
+ @click="navigateTo($options.tabNames.needs)"
>
- <dag />
+ <router-view />
</gl-tab>
- <gl-tab :active="isActive($options.tabNames.jobs)" data-testid="jobs-tab" lazy>
+ <gl-tab
+ :active="isActive($options.tabNames.jobs)"
+ data-testid="jobs-tab"
+ lazy
+ @click="navigateTo($options.tabNames.jobs)"
+ >
<template #title>
<span class="gl-mr-2">{{ $options.i18n.tabs.jobsTitle }}</span>
<gl-badge size="sm" data-testid="builds-counter">{{ totalJobCount }}</gl-badge>
</template>
- <jobs-app />
+ <router-view />
</gl-tab>
<gl-tab
v-if="showFailedJobsTab"
@@ -86,19 +104,25 @@ export default {
:active="isActive($options.tabNames.failures)"
data-testid="failed-jobs-tab"
lazy
+ @click="navigateTo($options.tabNames.failures)"
>
<template #title>
<span class="gl-mr-2">{{ $options.i18n.tabs.failedJobsTitle }}</span>
<gl-badge size="sm" data-testid="failed-builds-counter">{{ failedJobsCount }}</gl-badge>
</template>
- <failed-jobs-app :failed-jobs-summary="failedJobsSummary" />
+ <router-view :failed-jobs-summary="failedJobsSummary" />
</gl-tab>
- <gl-tab :active="isActive($options.tabNames.tests)" data-testid="tests-tab" lazy>
+ <gl-tab
+ :active="isActive($options.tabNames.tests)"
+ data-testid="tests-tab"
+ lazy
+ @click="navigateTo($options.tabNames.tests)"
+ >
<template #title>
<span class="gl-mr-2">{{ $options.i18n.tabs.testsTitle }}</span>
<gl-badge size="sm" data-testid="tests-counter">{{ testsCount }}</gl-badge>
</template>
- <test-reports />
+ <router-view />
</gl-tab>
<slot></slot>
</gl-tabs>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index 39d41415456..fe2ef2c2d71 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -115,6 +115,9 @@ export default {
commitTitle() {
return this.pipeline?.commit?.title;
},
+ pipelineName() {
+ return this.pipeline?.name;
+ },
},
methods: {
trackClick(action) {
@@ -125,7 +128,18 @@ export default {
</script>
<template>
<div class="pipeline-tags" data-testid="pipeline-url-table-cell">
- <div class="commit-title gl-mb-2" data-testid="commit-title-container">
+ <div v-if="pipelineName" class="gl-mb-2" data-testid="pipeline-name-container">
+ <span class="gl-display-flex">
+ <tooltip-on-truncate
+ :title="pipelineName"
+ class="gl-flex-grow-1 gl-text-truncate gl-text-gray-900"
+ >
+ {{ pipelineName }}
+ </tooltip-on-truncate>
+ </span>
+ </div>
+
+ <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">
<gl-link
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
index 387438fb726..cd44c998074 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
@@ -1,12 +1,14 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { formatDate, getTimeago, durationTimeFormatted } from '~/lib/utils/datetime_utility';
+import { durationTimeFormatted } from '~/lib/utils/datetime_utility';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: { GlIcon },
+ mixins: [timeagoMixin],
props: {
pipeline: {
type: Object,
@@ -35,12 +37,6 @@ export default {
showSkipped() {
return !this.duration && !this.finishedTime && this.skipped;
},
- timeFormatted() {
- return getTimeago().format(this.finishedTime);
- },
- tooltipTitle() {
- return formatDate(this.finishedTime);
- },
},
};
</script>
@@ -73,12 +69,12 @@ export default {
<time
v-gl-tooltip
- :title="tooltipTitle"
+ :title="tooltipTitle(finishedTime)"
:datetime="finishedTime"
data-placement="top"
data-container="body"
>
- {{ timeFormatted }}
+ {{ timeFormatted(finishedTime) }}
</time>
</p>
</div>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
index 69509c9088b..2d1f1945e5a 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
@@ -73,6 +73,7 @@ export default {
<template>
<gl-modal
+ data-testid="test-case-details-modal"
:modal-id="modalId"
:title="testCase.classname"
:action-primary="$options.modalCloseButton"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index 7d0f1ba4b5f..1cd28e027f3 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -112,21 +112,21 @@ export default {
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
- <div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
+ <div class="table-mobile-content gl-pr-0 gl-sm-pr-2 gl-overflow-wrap-break">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
</div>
</div>
<div class="table-section section-40 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
- <div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
+ <div class="table-mobile-content gl-pr-0 gl-sm-pr-2 gl-overflow-wrap-break">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.name" />
</div>
</div>
<div class="table-section section-10 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
- <div class="table-mobile-content gl-md-pr-2 gl-overflow-wrap-break">
+ <div class="table-mobile-content gl-pr-0 gl-sm-pr-2 gl-overflow-wrap-break">
<gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank">
<gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
</gl-link>
@@ -156,7 +156,7 @@ export default {
<div role="rowheader" class="table-mobile-header">
{{ __('Duration') }}
</div>
- <div class="table-mobile-content gl-sm-pr-2">
+ <div class="table-mobile-content gl-pr-0 gl-sm-pr-2">
{{ testCase.formattedTime }}
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 327633dcb1a..ed8ec614304 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -47,8 +47,7 @@ export const CHILD_VIEW = 'child';
// Pipeline tabs
-export const TAB_QUERY_PARAM = 'tab';
-
+export const pipelineTabName = 'graph';
export const needsTabName = 'dag';
export const jobsTabName = 'builds';
export const failedJobsTabName = 'failures';
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 3744649e9d5..1bbdd3625be 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,3 +1,4 @@
+import VueRouter from 'vue-router';
import { createAlert } from '~/flash';
import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag';
@@ -32,9 +33,16 @@ export default async function initPipelineDetailsBundle() {
if (gon.features?.pipelineTabsVue) {
const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
+ const { routes } = await import('ee_else_ce/pipelines/routes');
+
+ const router = new VueRouter({
+ mode: 'history',
+ base: dataset.pipelinePath,
+ routes,
+ });
try {
- const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider);
+ const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider, router);
createPipelineTabs(appOptions);
} catch {
createAlert({
diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js
index 508f188c229..d0ee6871a48 100644
--- a/app/assets/javascripts/pipelines/pipeline_tabs.js
+++ b/app/assets/javascripts/pipelines/pipeline_tabs.js
@@ -1,17 +1,17 @@
import Vue from 'vue';
+import VueRouter from 'vue-router';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue';
-import { removeParams, updateHistory } from '~/lib/utils/url_utility';
-import { TAB_QUERY_PARAM } from '~/pipelines/constants';
import { parseBoolean } from '~/lib/utils/common_utils';
import createTestReportsStore from './stores/test_reports';
import { getPipelineDefaultTab, reportToSentry } from './utils';
Vue.use(VueApollo);
+Vue.use(VueRouter);
Vue.use(Vuex);
-export const createAppOptions = (selector, apolloProvider) => {
+export const createAppOptions = (selector, apolloProvider, router) => {
const el = document.querySelector(selector);
if (!el) return null;
@@ -40,6 +40,7 @@ export const createAppOptions = (selector, apolloProvider) => {
suiteEndpoint,
blobPath,
hasTestReport,
+ emptyDagSvgPath,
emptyStateImagePath,
artifactsExpiredImagePath,
isFullCodequalityReportAvailable,
@@ -65,6 +66,7 @@ export const createAppOptions = (selector, apolloProvider) => {
}),
},
}),
+ router,
provide: {
canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports),
codequalityReportDownloadPath,
@@ -91,6 +93,7 @@ export const createAppOptions = (selector, apolloProvider) => {
suiteEndpoint,
blobPath,
hasTestReport,
+ emptyDagSvgPath,
emptyStateImagePath,
artifactsExpiredImagePath,
testsCount,
@@ -107,12 +110,6 @@ export const createAppOptions = (selector, apolloProvider) => {
export const createPipelineTabs = (options) => {
if (!options) return;
- updateHistory({
- url: removeParams([TAB_QUERY_PARAM]),
- title: document.title,
- replace: true,
- });
-
// eslint-disable-next-line no-new
new Vue(options);
};
diff --git a/app/assets/javascripts/pipelines/routes.js b/app/assets/javascripts/pipelines/routes.js
new file mode 100644
index 00000000000..0e1414ec390
--- /dev/null
+++ b/app/assets/javascripts/pipelines/routes.js
@@ -0,0 +1,20 @@
+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 {
+ pipelineTabName,
+ needsTabName,
+ jobsTabName,
+ failedJobsTabName,
+ testReportTabName,
+} from './constants';
+
+export const routes = [
+ { name: pipelineTabName, path: '/', component: PipelineGraphWrapper },
+ { name: needsTabName, path: '/dag', component: Dag },
+ { name: jobsTabName, path: '/builds', component: JobsApp },
+ { name: failedJobsTabName, path: '/failures', component: FailedJobsApp },
+ { name: testReportTabName, path: '/test_report', component: TestReports },
+];
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 588d15495ab..b8276327843 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,12 +1,7 @@
import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
-import { getParameterValues } from '~/lib/utils/url_utility';
-import {
- NEEDS_PROPERTY,
- SUPPORTED_FILTER_PARAMETERS,
- TAB_QUERY_PARAM,
- validPipelineTabNames,
-} from './constants';
+import { parseUrlPathname } from '~/lib/utils/url_utility';
+import { NEEDS_PROPERTY, SUPPORTED_FILTER_PARAMETERS, validPipelineTabNames } 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.
@@ -145,10 +140,12 @@ export const reportMessageToSentry = (component, message, context) => {
};
export const getPipelineDefaultTab = (url) => {
- const [tabQueryValue] = getParameterValues(TAB_QUERY_PARAM, url);
+ const strippedUrl = parseUrlPathname(url);
+ const regexp = /\w*$/;
+ const [tabName] = strippedUrl.match(regexp);
- if (tabQueryValue && validPipelineTabNames.includes(tabQueryValue)) {
- return tabQueryValue;
+ if (tabName && validPipelineTabNames.includes(tabName)) {
+ return tabName;
}
return null;
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 2802e4a90b9..0256eec6d56 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
@@ -124,7 +124,7 @@ export default {
</script>
<template>
- <div class="gl-pt-2">
+ <div>
<gl-loading-icon v-if="$apollo.queries.pipeline.loading" />
<pipeline-mini-graph
v-else
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 603fdfdf80a..9365066418b 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -3,6 +3,7 @@ import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { ACTIVE_AND_BLOCKED_USER_STATES } from '~/users_select/constants';
import * as types from './mutation_types';
export default {
@@ -23,7 +24,7 @@ export default {
.get(joinPaths(gon.relative_url_root || '', '/-/autocomplete/users.json'), {
params: {
project_id: projectId,
- active: true,
+ states: ACTIVE_AND_BLOCKED_USER_STATES,
search: author,
},
})
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index 271694863e8..e4d5e5bd233 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -131,7 +131,7 @@ export default {
<revision-card
data-testid="sourceRevisionCard"
:refs-project-path="to.refsProjectPath"
- revision-text="Source"
+ :revision-text="__('Source')"
params-name="to"
:params-branch="to.revision"
:projects="to.projects"
@@ -160,7 +160,7 @@ export default {
<revision-card
data-testid="targetRevisionCard"
:refs-project-path="from.refsProjectPath"
- revision-text="Target"
+ :revision-text="__('Target')"
params-name="from"
:params-branch="from.revision"
:projects="from.projects"
diff --git a/app/assets/javascripts/projects/components/shared/delete_button.vue b/app/assets/javascripts/projects/components/shared/delete_button.vue
index 277af2f281e..64a16b462f5 100644
--- a/app/assets/javascripts/projects/components/shared/delete_button.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_button.vue
@@ -62,7 +62,11 @@ export default {
return {
primary: {
text: __('Yes, delete project'),
- attributes: [{ variant: 'danger' }, { disabled: this.confirmDisabled }],
+ attributes: [
+ { variant: 'danger' },
+ { disabled: this.confirmDisabled },
+ { 'data-qa-selector': 'confirm_delete_button' },
+ ],
},
cancel: {
text: __('Cancel, keep project'),
@@ -97,9 +101,13 @@ export default {
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
- <gl-button v-gl-modal="modalId" category="primary" variant="danger">{{
- $options.strings.deleteProject
- }}</gl-button>
+ <gl-button
+ v-gl-modal="modalId"
+ category="primary"
+ variant="danger"
+ data-qa-selector="delete_button"
+ >{{ $options.strings.deleteProject }}</gl-button
+ >
<gl-modal
ref="removeModal"
@@ -168,6 +176,7 @@ export default {
v-model="userInput"
name="confirm_name_input"
type="text"
+ data-qa-selector="confirm_name_field"
/>
<slot name="modal-footer"></slot>
</div>
diff --git a/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue b/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue
index e42d9154866..c23644b40d2 100644
--- a/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_push_tip_popover.vue
@@ -56,7 +56,7 @@ export default {
</p>
<p>
<a
- :href="`${workingWithProjectsHelpPath}#push-to-create-a-new-project`"
+ :href="`${workingWithProjectsHelpPath}#create-a-new-project-with-git-push`"
class="gl-font-sm"
target="_blank"
>{{ $options.i18n.helpLinkText }}</a
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 186fcf70838..4643bfe58f6 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -2,6 +2,8 @@
import { GlTabs, GlTab } from '@gitlab/ui';
import API from '~/api';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
+import { SNOWPLOW_DATA_SOURCE, SNOWPLOW_LABEL, SNOWPLOW_SCHEMA } from '../constants';
import PipelineCharts from './pipeline_charts.vue';
export default {
@@ -23,6 +25,7 @@ export default {
leadTimeTabEvent: 'p_analytics_ci_cd_lead_time',
timeToRestoreServiceTabEvent: 'p_analytics_ci_cd_time_to_restore_service',
changeFailureRateTabEvent: 'p_analytics_ci_cd_change_failure_rate',
+ mixins: [Tracking.mixin()],
inject: {
shouldRenderDoraCharts: {
type: Boolean,
@@ -75,8 +78,21 @@ export default {
updateHistory({ url: path, title: window.title });
}
},
- trackTabClick(tab) {
- API.trackRedisHllUserEvent(tab);
+ trackTabClick(event, snowplow = false) {
+ API.trackRedisHllUserEvent(event);
+
+ if (snowplow) {
+ this.track('click_tab', {
+ label: SNOWPLOW_LABEL,
+ context: {
+ schema: SNOWPLOW_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: SNOWPLOW_DATA_SOURCE,
+ },
+ },
+ });
+ }
},
},
};
@@ -87,7 +103,7 @@ export default {
<gl-tab
:title="__('Pipelines')"
data-testid="pipelines-tab"
- @click="trackTabClick($options.piplelinesTabEvent)"
+ @click="trackTabClick($options.piplelinesTabEvent, true)"
>
<pipeline-charts />
</gl-tab>
@@ -95,14 +111,14 @@ export default {
<gl-tab
:title="__('Deployment frequency')"
data-testid="deployment-frequency-tab"
- @click="trackTabClick($options.deploymentFrequencyTabEvent)"
+ @click="trackTabClick($options.deploymentFrequencyTabEvent, true)"
>
<deployment-frequency-charts />
</gl-tab>
<gl-tab
:title="__('Lead time')"
data-testid="lead-time-tab"
- @click="trackTabClick($options.leadTimeTabEvent)"
+ @click="trackTabClick($options.leadTimeTabEvent, true)"
>
<lead-time-charts />
</gl-tab>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index 41fe81f21ea..c13824a9952 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -19,3 +19,7 @@ export const PARSE_FAILURE = 'parse_failure';
export const LOAD_ANALYTICS_FAILURE = 'load_analytics_failure';
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_DATA_SOURCE = 'redis_hll';
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 10f6c28a7bf..df99aac6b9e 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -12,7 +12,7 @@ const buildUrl = (urlRoot, url) => {
return newUrl;
};
-export const getUsers = (query) => {
+export const getUsers = (query, states) => {
return axios.get(buildUrl(gon.relative_url_root || '', USERS_PATH), {
params: {
search: query,
@@ -20,6 +20,7 @@ export const getUsers = (query) => {
active: true,
project_id: gon.current_project_id,
push_code: true,
+ states,
},
});
};
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
index 264c2629433..6da058ebc9c 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
@@ -21,9 +21,14 @@ export const I18N = {
approvalsTitle: s__('BranchRules|Approvals'),
manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'),
approvalsDescription: s__(
- 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}',
+ 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Learn more.%{linkEnd}',
),
statusChecksTitle: s__('BranchRules|Status checks'),
+ statusChecksDescription: s__(
+ 'BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}',
+ ),
+ statusChecksLinkTitle: s__('BranchRules|Manage in Status checks'),
+ statusChecksHeader: s__('BranchRules|Status checks (%{total})'),
allowedToPushHeader: s__('BranchRules|Allowed to push (%{total})'),
allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'),
approvalsHeader: s__('BranchRules|Required approvals (%{total})'),
@@ -40,3 +45,5 @@ export const WILDCARDS_HELP_PATH =
export const PROTECTED_BRANCHES_HELP_PATH = 'user/project/protected_branches';
export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index.md';
+
+export const STATUS_CHECKS_HELP_PATH = 'user/project/merge_requests/status_checks.md';
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index 318940478a8..eb11e17dd1b 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -12,11 +12,13 @@ import {
WILDCARDS_HELP_PATH,
PROTECTED_BRANCHES_HELP_PATH,
APPROVALS_HELP_PATH,
+ STATUS_CHECKS_HELP_PATH,
} from './constants';
const wildcardsHelpDocLink = helpPagePath(WILDCARDS_HELP_PATH);
const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH);
const approvalsHelpDocLink = helpPagePath(APPROVALS_HELP_PATH);
+const statusChecksHelpDocLink = helpPagePath(STATUS_CHECKS_HELP_PATH);
export default {
name: 'RuleView',
@@ -24,6 +26,7 @@ export default {
wildcardsHelpDocLink,
protectedBranchesHelpDocLink,
approvalsHelpDocLink,
+ statusChecksHelpDocLink,
components: { Protection, GlSprintf, GlLink, GlLoadingIcon },
inject: {
projectPath: {
@@ -35,6 +38,9 @@ export default {
approvalRulesPath: {
default: '',
},
+ statusChecksPath: {
+ default: '',
+ },
},
apollo: {
project: {
@@ -45,18 +51,19 @@ export default {
};
},
update({ project: { branchRules } }) {
- this.branchProtection = branchRules.nodes.find(
- (rule) => rule.name === this.branch,
- )?.branchProtection;
+ const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch);
+ this.branchProtection = branchRule?.branchProtection;
+ this.approvalRules = branchRule?.approvalRules;
+ this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
},
},
},
data() {
return {
branch: getParameterByName(BRANCH_PARAM_NAME),
- branchProtection: {
- approvalRules: {},
- },
+ branchProtection: {},
+ approvalRules: {},
+ statusChecks: [],
};
},
computed: {
@@ -92,6 +99,11 @@ export default {
total,
});
},
+ statusChecksHeader() {
+ return sprintf(this.$options.i18n.statusChecksHeader, {
+ total: this.statusChecks.length,
+ });
+ },
allBranches() {
return this.branch === ALL_BRANCHES_WILDCARD;
},
@@ -104,7 +116,7 @@ export default {
: this.$options.i18n.branchNameOrPattern;
},
approvals() {
- return this.branchProtection?.approvalRules?.nodes || [];
+ return this.approvalRules?.nodes || [];
},
},
methods: {
@@ -202,6 +214,21 @@ export default {
/>
<!-- Status checks -->
- <!-- Follow-up: add status checks section (https://gitlab.com/gitlab-org/gitlab/-/issues/372362) -->
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.statusChecksTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.statusChecksDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.statusChecksHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <protection
+ class="gl-mt-3"
+ :header="statusChecksHeader"
+ :header-link-title="$options.i18n.statusChecksLinkTitle"
+ :header-link-href="statusChecksPath"
+ :status-checks="statusChecks"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
index cfe2df0dbda..813c667dcdd 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: () => [],
},
+ statusChecks: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
showUsersDivider() {
@@ -95,5 +100,14 @@ export default {
:users="approval.eligibleApprovers.nodes"
:approvals-required="approval.approvalsRequired"
/>
+
+ <!-- Status checks -->
+ <protection-row
+ v-for="(statusCheck, index) in statusChecks"
+ :key="statusCheck.id"
+ :show-divider="index !== 0"
+ :title="statusCheck.name"
+ :status-check-url="statusCheck.externalUrl"
+ />
</gl-card>
</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
index 28a1c09fa82..9bff2f5506c 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -41,6 +41,11 @@ export default {
required: false,
default: 0,
},
+ statusCheckUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
avatarBadgeSrOnlyText() {
@@ -64,10 +69,10 @@ export default {
<template>
<div
- class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4"
+ class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4 gl-border-t-1"
:class="{ 'gl-border-t-solid': showDivider }"
>
- <div class="gl-display-flex gl-w-half gl-justify-content-space-between">
+ <div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-align-items-center">
<div class="gl-mr-7 gl-w-quarter">{{ title }}</div>
<gl-avatars-inline
@@ -94,6 +99,8 @@ export default {
</template>
</gl-avatars-inline>
+ <div v-if="statusCheckUrl" class="gl-ml-7 gl-flex-grow-1">{{ statusCheckUrl }}</div>
+
<div
v-for="(item, index) in accessLevels"
:key="index"
@@ -104,7 +111,7 @@ export default {
{{ item.accessLevelDescription }}
</div>
- <div class="gl-ml-7 gl-w-quarter">{{ approvalsRequiredTitle }}</div>
+ <div class="gl-ml-7 gl-flex-grow-1">{{ approvalsRequiredTitle }}</div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
index 07fd0a7080f..89cfb1e1c8e 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -14,7 +14,7 @@ export default function mountBranchRules(el) {
defaultClient: createDefaultClient(),
});
- const { projectPath, protectedBranchesPath, approvalRulesPath } = el.dataset;
+ const { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath } = el.dataset;
return new Vue({
el,
@@ -23,6 +23,7 @@ export default function mountBranchRules(el) {
projectPath,
protectedBranchesPath,
approvalRulesPath,
+ statusChecksPath,
},
render(h) {
return h(View);
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
index 3ac165498a1..aa1e4923aa8 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -44,6 +44,30 @@ query getBranchRulesDetails($projectPath: ID!) {
}
}
}
+ approvalRules {
+ nodes {
+ id
+ name
+ type
+ approvalsRequired
+ eligibleApprovers {
+ nodes {
+ id
+ name
+ username
+ webUrl
+ avatarUrl
+ }
+ }
+ }
+ }
+ externalStatusChecks {
+ nodes {
+ id
+ name
+ externalUrl
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index 55420c9c732..fd5fabd7c8a 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -1,30 +1,14 @@
<script>
-import { GlFormGroup, GlAlert } from '@gitlab/ui';
-import { debounce } from 'lodash';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
import { getTransferLocations } from '~/api/projects_api';
-import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import { s__, __ } from '~/locale';
-import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql';
export default {
name: 'TransferProjectForm',
components: {
- GlFormGroup,
- NamespaceSelect,
+ TransferLocations,
ConfirmDanger,
- GlAlert,
},
- i18n: {
- errorMessage: s__(
- 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
- ),
- alertDismissAlert: __('Dismiss'),
- },
- inject: ['projectId'],
props: {
confirmationPhrase: {
type: String,
@@ -37,150 +21,37 @@ export default {
},
data() {
return {
- userNamespaces: [],
- groupNamespaces: [],
- initialNamespacesLoaded: false,
- selectedNamespace: null,
- hasError: false,
- isLoading: false,
- isSearchLoading: false,
- searchTerm: '',
- page: 1,
- totalPages: 1,
+ selectedTransferLocation: null,
};
},
+
computed: {
hasSelectedNamespace() {
- return Boolean(this.selectedNamespace?.id);
+ return Boolean(this.selectedTransferLocation?.id);
},
- hasNextPageOfGroups() {
- return this.page < this.totalPages;
+ },
+ watch: {
+ selectedTransferLocation(selectedTransferLocation) {
+ this.$emit('selectTransferLocation', selectedTransferLocation.id);
},
},
methods: {
- async handleShow() {
- if (this.initialNamespacesLoaded) {
- return;
- }
-
- this.isLoading = true;
-
- [this.groupNamespaces, this.userNamespaces] = await Promise.all([
- this.getGroupNamespaces(),
- this.getUserNamespaces(),
- ]);
-
- this.isLoading = false;
- this.initialNamespacesLoaded = true;
- },
- handleSelect(selectedNamespace) {
- this.selectedNamespace = selectedNamespace;
- this.$emit('selectNamespace', selectedNamespace.id);
- },
- async getGroupNamespaces() {
- try {
- const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, {
- page: this.page,
- search: this.searchTerm,
- });
-
- const { totalPages } = parseIntPagination(normalizeHeaders(headers));
- this.totalPages = totalPages;
-
- return groupNamespaces.map(({ id, full_name: humanName }) => ({
- id,
- humanName,
- }));
- } catch (error) {
- this.hasError = true;
-
- return [];
- }
- },
- async getUserNamespaces() {
- try {
- const {
- data: {
- currentUser: { namespace },
- },
- } = await this.$apollo.query({
- query: currentUserNamespace,
- });
-
- if (!namespace) {
- return [];
- }
-
- return [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ];
- } catch (error) {
- this.hasError = true;
-
- return [];
- }
- },
- async handleLoadMoreGroups() {
- this.isLoading = true;
- this.page += 1;
-
- const groupNamespaces = await this.getGroupNamespaces();
- this.groupNamespaces.push(...groupNamespaces);
-
- this.isLoading = false;
- },
- debouncedSearch: debounce(async function debouncedSearch() {
- this.isSearchLoading = true;
-
- this.groupNamespaces = await this.getGroupNamespaces();
-
- this.isSearchLoading = false;
- }, DEBOUNCE_DELAY),
- handleSearch(searchTerm) {
- this.searchTerm = searchTerm;
- this.page = 1;
-
- this.debouncedSearch();
- },
- handleAlertDismiss() {
- this.hasError = false;
- },
+ getTransferLocations,
},
};
</script>
<template>
<div>
- <gl-alert
- v-if="hasError"
- variant="danger"
- :dismiss-label="$options.i18n.alertDismissLabel"
- @dismiss="handleAlertDismiss"
- >{{ $options.i18n.errorMessage }}</gl-alert
- >
- <gl-form-group>
- <namespace-select
- data-testid="transfer-project-namespace"
- :full-width="true"
- :group-namespaces="groupNamespaces"
- :user-namespaces="userNamespaces"
- :selected-namespace="selectedNamespace"
- :has-next-page-of-groups="hasNextPageOfGroups"
- :is-loading="isLoading"
- :is-search-loading="isSearchLoading"
- :should-filter-namespaces="false"
- @select="handleSelect"
- @load-more-groups="handleLoadMoreGroups"
- @search="handleSearch"
- @show="handleShow"
- />
- </gl-form-group>
+ <transfer-locations
+ v-model="selectedTransferLocation"
+ data-testid="transfer-project-namespace"
+ :group-transfer-locations-api-method="getTransferLocations"
+ />
<confirm-danger
:disabled="!hasSelectedNamespace"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
+ button-qa-selector="transfer_project_button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
index 89c158a9ba8..7f810e647ae 100644
--- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js
+++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
@@ -12,7 +12,7 @@ export default () => {
Vue.use(VueApollo);
const {
- projectId,
+ projectId: resourceId,
targetFormId = null,
targetHiddenInputId = null,
buttonText: confirmButtonText = '',
@@ -27,7 +27,7 @@ export default () => {
}),
provide: {
confirmDangerMessage,
- projectId,
+ resourceId,
},
render(createElement) {
return createElement(TransferProjectForm, {
@@ -36,7 +36,7 @@ export default () => {
confirmationPhrase,
},
on: {
- selectNamespace: (id) => {
+ selectTransferLocation: (id) => {
if (targetHiddenInputId && document.getElementById(targetHiddenInputId)) {
document.getElementById(targetHiddenInputId).value = id;
}
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 94793a535cc..a9eb2a53fbf 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -50,7 +50,15 @@ export default {
<template>
<div class="settings-content">
- <branch-rule v-for="rule in branchRules" :key="rule.name" :name="rule.name" />
+ <branch-rule
+ v-for="rule in branchRules"
+ :key="rule.name"
+ :name="rule.name"
+ :is-default="rule.isDefault"
+ :branch-protection="rule.branchProtection"
+ :status-checks-total="rule.externalStatusChecks.nodes.length"
+ :approval-rules-total="rule.approvalRules.nodes.length"
+ />
<span v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</span>
</div>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 2b88f8561d7..78c824c66d1 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -1,11 +1,14 @@
<script>
import { GlBadge, GlButton } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { s__, sprintf, n__ } from '~/locale';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
- protectedLabel: s__('BranchRules|protected'),
detailsButtonLabel: s__('BranchRules|Details'),
+ allowForcePush: s__('BranchRules|Allowed to force push'),
+ codeOwnerApprovalRequired: s__('BranchRules|Requires CODEOWNERS approval'),
+ statusChecks: s__('BranchRules|%{total} status %{subject}'),
+ approvalRules: s__('BranchRules|%{total} approval %{subject}'),
};
export default {
@@ -30,24 +33,57 @@ export default {
required: false,
default: false,
},
- isProtected: {
- type: Boolean,
+ branchProtection: {
+ type: Object,
required: false,
- default: false,
+ default: () => {},
},
- approvalDetails: {
- type: Array,
+ statusChecksTotal: {
+ type: Number,
required: false,
- default: () => [],
+ default: 0,
+ },
+ approvalRulesTotal: {
+ type: Number,
+ required: false,
+ default: 0,
},
},
computed: {
hasApprovalDetails() {
- return this.approvalDetails && this.approvalDetails.length;
+ return this.approvalDetails.length;
},
detailsPath() {
return `${this.branchRulesPath}?branch=${this.name}`;
},
+ statusChecksText() {
+ return sprintf(this.$options.i18n.statusChecks, {
+ total: this.statusChecksTotal,
+ subject: n__('check', 'checks', this.statusChecksTotal),
+ });
+ },
+ approvalRulesText() {
+ return sprintf(this.$options.i18n.approvalRules, {
+ total: this.approvalRulesTotal,
+ subject: n__('rule', 'rules', this.approvalRulesTotal),
+ });
+ },
+ approvalDetails() {
+ const approvalDetails = [];
+ if (this.branchProtection.allowForcePush) {
+ approvalDetails.push(this.$options.i18n.allowForcePush);
+ }
+ if (this.branchProtection.codeOwnerApprovalRequired) {
+ approvalDetails.push(this.$options.i18n.codeOwnerApprovalRequired);
+ }
+ if (this.statusChecksTotal) {
+ approvalDetails.push(this.statusChecksText);
+ }
+ if (this.approvalRulesTotal) {
+ approvalDetails.push(this.approvalRulesText);
+ }
+ return approvalDetails;
+ },
},
};
</script>
@@ -61,14 +97,12 @@ export default {
$options.i18n.defaultLabel
}}</gl-badge>
- <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
- $options.i18n.protectedLabel
- }}</gl-badge>
-
<ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
<li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
</ul>
</div>
- <gl-button :href="detailsPath"> {{ $options.i18n.detailsButtonLabel }}</gl-button>
+ <gl-button class="gl-align-self-start" :href="detailsPath">
+ {{ $options.i18n.detailsButtonLabel }}</gl-button
+ >
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
index 104a0c25a80..49e089e7805 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
@@ -4,6 +4,21 @@ query getBranchRules($projectPath: ID!) {
branchRules {
nodes {
name
+ isDefault
+ branchProtection {
+ allowForcePush
+ codeOwnerApprovalRequired
+ }
+ externalStatusChecks {
+ nodes {
+ id
+ }
+ }
+ approvalRules {
+ nodes {
+ id
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 452e7a4fd21..85550e262e6 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -265,35 +265,14 @@ export default {
class="mt-3"
>
<gl-form-input
- v-if="hasProjectKeySupport"
id="service-desk-email-from-name"
v-model.trim="outgoingName"
data-testid="email-from-name"
/>
- <template v-if="hasProjectKeySupport" #description>
+ <template #description>
{{ __('Name to be used as the sender for emails from Service Desk.') }}
</template>
- <template v-else #description>
- <span class="gl-text-gray-900">
- <gl-sprintf
- :message="
- __(
- 'To add display name, set up a Service Desk email address. %{linkStart}Learn more.%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link
- :href="customEmailAddressHelpUrl"
- target="_blank"
- class="gl-text-blue-600 font-size-inherit"
- >{{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </span>
- </template>
</gl-form-group>
<gl-button
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 1ab41ee2f0a..4a130ade631 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import {
issuableIconMap,
linkedIssueTypesMap,
@@ -95,6 +95,16 @@ export default {
required: false,
default: true,
},
+ hasError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ itemAddFailureMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -120,9 +130,6 @@ export default {
shouldShowTokenBody() {
return this.hasRelatedIssues || this.isFetching;
},
- hasBody() {
- return this.isFormVisible || this.shouldShowTokenBody;
- },
headerText() {
return issuablesBlockHeaderTextMap[this.issuableType];
},
@@ -147,6 +154,11 @@ export default {
toggleLabel() {
return this.isOpen ? __('Collapse') : __('Expand');
},
+ emptyStateMessage() {
+ return this.showCategorizedIssues
+ ? sprintf(this.$options.i18n.emptyItemsPremium, { issuableType: this.issuableType })
+ : sprintf(this.$options.i18n.emptyItemsFree, { issuableType: this.issuableType });
+ },
},
methods: {
handleToggle() {
@@ -158,6 +170,12 @@ export default {
},
},
linkedIssueTypesTextMap,
+ i18n: {
+ emptyItemsFree: __("Link %{issuableType}s together to show that they're related."),
+ emptyItemsPremium: __(
+ "Link %{issuableType}s together to show that they're related or that one is blocking others.",
+ ),
+ },
};
</script>
@@ -166,7 +184,6 @@ export default {
<div class="card card-slim gl-overflow-hidden gl-mt-5 gl-mb-0">
<div
:class="{
- 'panel-empty-heading border-bottom-0': !hasBody,
'gl-border-b-1': isOpen,
'gl-border-b-0': !isOpen,
}"
@@ -180,16 +197,6 @@ export default {
aria-hidden="true"
/>
<slot name="header-text">{{ headerText }}</slot>
- <gl-link
- v-if="hasHelpPath"
- :href="helpPath"
- target="_blank"
- class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
- data-testid="help-link"
- :aria-label="helpLinkText"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
<div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-3">
<span class="gl-display-inline-flex gl-align-items-center">
@@ -216,7 +223,6 @@ export default {
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
- :disabled="!hasRelatedIssues"
data-testid="toggle-links"
@click="handleToggle"
/>
@@ -233,7 +239,7 @@ export default {
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area card-body bordered-box bg-white"
- :class="{ 'gl-mb-5': shouldShowTokenBody }"
+ :class="{ 'gl-mb-5': shouldShowTokenBody, 'gl-show-field-errors': hasError }"
>
<add-issuable-form
:show-categorized-issues="showCategorizedIssues"
@@ -245,6 +251,8 @@ export default {
:auto-complete-epics="autoCompleteEpics"
:auto-complete-issues="autoCompleteIssues"
:path-id-separator="pathIdSeparator"
+ :has-error="hasError"
+ :item-add-failure-message="itemAddFailureMessage"
@pendingIssuableRemoveRequest="$emit('pendingIssuableRemoveRequest', $event)"
@addIssuableFormInput="$emit('addIssuableFormInput', $event)"
@addIssuableFormBlur="$emit('addIssuableFormBlur', $event)"
@@ -269,6 +277,20 @@ export default {
@saveReorder="$emit('saveReorder', $event)"
/>
</template>
+ <div v-if="!shouldShowTokenBody && !isFormVisible" data-testid="related-items-empty">
+ <p class="gl-my-5 gl-px-5">
+ {{ emptyStateMessage }}
+ <gl-link
+ v-if="hasHelpPath"
+ :href="helpPath"
+ target="_blank"
+ data-testid="help-link"
+ :aria-label="helpLinkText"
+ >
+ {{ __('Learn more.') }}
+ </gl-link>
+ </p>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index 38e1d6e9d4f..795eb3b0083 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -107,6 +107,8 @@ export default {
isSubmitting: false,
isFormVisible: false,
inputValue: '',
+ hasError: false,
+ errorMessage: null,
};
},
computed: {
@@ -170,11 +172,11 @@ export default {
this.isFormVisible = false;
})
.catch(({ response }) => {
- let errorMessage = addRelatedIssueErrorMap[this.issuableType];
+ this.hasError = true;
+ this.errorMessage = addRelatedIssueErrorMap[this.issuableType];
if (response && response.data && response.data.message) {
- errorMessage = response.data.message;
+ this.errorMessage = response.data.message;
}
- createAlert({ message: errorMessage });
})
.finally(() => {
this.isSubmitting = false;
@@ -266,6 +268,8 @@ export default {
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
:show-categorized-issues="showCategorizedIssues"
+ :has-error="hasError"
+ :item-add-failure-message="errorMessage"
@saveReorder="saveIssueOrder"
@toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm"
@addIssuableFormInput="onInput"
diff --git a/app/assets/javascripts/related_issues/constants.js b/app/assets/javascripts/related_issues/constants.js
index 4eb054ccb5c..d1b2d41d7ae 100644
--- a/app/assets/javascripts/related_issues/constants.js
+++ b/app/assets/javascripts/related_issues/constants.js
@@ -111,8 +111,9 @@ export const issuablesBlockHeaderTextMap = {
};
export const issuablesBlockHelpTextMap = {
- [issuableTypesMap.ISSUE]: __('Read more about related issues'),
- [issuableTypesMap.EPIC]: __('Read more about related epics'),
+ [issuableTypesMap.ISSUE]: __('Learn more about linking issues'),
+ [issuableTypesMap.INCIDENT]: __('Learn more about linking issues and incidents'),
+ [issuableTypesMap.EPIC]: __('Learn more about linking epics'),
};
export const issuablesBlockAddButtonTextMap = {
diff --git a/app/assets/javascripts/related_issues/index.js b/app/assets/javascripts/related_issues/index.js
index eb2f5d119b8..c77a67c4287 100644
--- a/app/assets/javascripts/related_issues/index.js
+++ b/app/assets/javascripts/related_issues/index.js
@@ -17,6 +17,7 @@ export function initRelatedIssues(issueType = 'issue') {
provide: {
fullPath: el.dataset.fullPath,
hasIssueWeightsFeature: parseBoolean(el.dataset.hasIssueWeightsFeature),
+ hasIterationsFeature: parseBoolean(el.dataset.hasIterationsFeature),
},
render: (createElement) =>
createElement(RelatedIssuesRoot, {
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index dd3f4ed636f..965b9fa09d6 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -257,9 +257,9 @@ export default {
<asset-links-form />
- <div class="d-flex pt-3">
+ <div class="d-flex gl-gap-x-3 pt-3">
<gl-button
- class="mr-auto js-no-auto-disable"
+ class="js-no-auto-disable"
category="primary"
variant="confirm"
type="submit"
diff --git a/app/assets/javascripts/releases/components/asset_links_form.vue b/app/assets/javascripts/releases/components/asset_links_form.vue
index 7c6d44456d9..dc465851721 100644
--- a/app/assets/javascripts/releases/components/asset_links_form.vue
+++ b/app/assets/javascripts/releases/components/asset_links_form.vue
@@ -131,10 +131,10 @@ export default {
<div
v-for="(link, index) in release.assets.links"
:key="link.id"
- class="row flex-column flex-sm-row align-items-stretch align-items-sm-start no-gutters"
+ class="gl-sm-display-flex flex-column flex-sm-row gl-gap-5 align-items-stretch align-items-sm-start no-gutters"
>
<gl-form-group
- class="url-field form-group col pr-sm-2"
+ class="url-field form-group col"
:label="__('URL')"
:label-for="`asset-url-${index}`"
>
@@ -174,7 +174,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="link-title-field col px-sm-2"
+ class="link-title-field col"
:label="__('Link title')"
:label-for="`asset-link-name-${index}`"
>
@@ -201,7 +201,7 @@ export default {
</gl-form-group>
<gl-form-group
- class="link-type-field col-auto px-sm-2"
+ class="link-type-field col-auto"
:label="__('Type')"
:label-for="`asset-type-${index}`"
>
@@ -216,9 +216,8 @@ export default {
/>
</gl-form-group>
- <div class="mb-5 mb-sm-3 mt-sm-4 col col-sm-auto pl-sm-2">
+ <div v-if="release.assets.links.length !== 1" class="mb-5 mb-sm-3 mt-sm-4 col col-sm-auto">
<gl-button
- v-gl-tooltip
class="remove-button w-100 form-control"
:aria-label="__('Remove asset link')"
:title="__('Remove asset link')"
@@ -233,8 +232,9 @@ export default {
</div>
<gl-button
ref="addAnotherLinkButton"
- variant="link"
- class="align-self-end mb-5 mb-sm-0"
+ category="secondary"
+ variant="confirm"
+ class="gl-align-self-start gl-mb-5"
@click="onAddAnotherClicked"
>
{{ __('Add another link') }}
diff --git a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue b/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
deleted file mode 100644
index 599e8d35708..00000000000
--- a/app/assets/javascripts/reports/codequality_report/grouped_codequality_reports_app.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
-import { s__, sprintf } from '~/locale';
-import { componentNames } from '~/reports/components/issue_body';
-import ReportSection from '~/reports/components/report_section.vue';
-import createStore from './store';
-
-export default {
- name: 'GroupedCodequalityReportsApp',
- store: createStore(),
- components: {
- ReportSection,
- },
- props: {
- headBlobPath: {
- type: String,
- required: true,
- },
- baseBlobPath: {
- type: String,
- required: false,
- default: null,
- },
- codequalityReportsPath: {
- type: String,
- required: false,
- default: '',
- },
- codequalityHelpPath: {
- type: String,
- required: true,
- },
- },
- componentNames,
- computed: {
- ...mapState(['newIssues', 'resolvedIssues', 'hasError', 'statusReason']),
- ...mapGetters([
- 'hasCodequalityIssues',
- 'codequalityStatus',
- 'codequalityText',
- 'codequalityPopover',
- ]),
- },
- created() {
- this.setPaths({
- baseBlobPath: this.baseBlobPath,
- headBlobPath: this.headBlobPath,
- reportsPath: this.codequalityReportsPath,
- helpPath: this.codequalityHelpPath,
- });
-
- this.fetchReports();
- },
- methods: {
- ...mapActions(['fetchReports', 'setPaths']),
- },
- loadingText: sprintf(s__('ciReport|Loading %{reportName} report'), {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- reportName: 'Code quality',
- }),
- errorText: sprintf(s__('ciReport|Failed to load %{reportName} report'), {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- reportName: 'Code quality',
- }),
-};
-</script>
-<template>
- <report-section
- :status="codequalityStatus"
- :loading-text="$options.loadingText"
- :error-text="$options.errorText"
- :success-text="codequalityText"
- :unresolved-issues="newIssues"
- :resolved-issues="resolvedIssues"
- :has-issues="hasCodequalityIssues"
- :component="$options.componentNames.CodequalityIssueBody"
- :popover-options="codequalityPopover"
- :show-report-section-status-icon="false"
- track-action="users_expanding_testing_code_quality_report"
- class="js-codequality-widget mr-widget-border-top mr-report"
- >
- <template v-if="hasError" #sub-heading>{{ statusReason }}</template>
- </report-section>
-</template>
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index a76a6f45c07..4f418216024 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -2,12 +2,10 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
export const components = {
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
- TestIssueBody: () => import('../grouped_test_report/components/test_issue_body.vue'),
};
export const componentNames = {
CodequalityIssueBody: 'CodequalityIssueBody',
- TestIssueBody: 'TestIssueBody',
};
export const iconComponents = {
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index bb86695b9a3..468c8916b8d 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -168,11 +168,6 @@ export default {
},
methods: {
toggleCollapsed() {
- // Because the top-level div is always clickable, we need to check if we can collapse.
- if (!this.isCollapsible) {
- return;
- }
-
if (this.trackAction) {
api.trackRedisHllUserEvent(this.trackAction);
}
@@ -187,7 +182,7 @@ export default {
</script>
<template>
<section class="media-section">
- <div class="media" :class="{ 'gl-cursor-pointer': isCollapsible }" @click="toggleCollapsed">
+ <div class="media">
<status-icon :status="statusIconName" :size="24" class="align-self-center" />
<div class="media-body gl-display-flex gl-align-items-flex-start gl-flex-direction-row!">
<div
@@ -218,7 +213,7 @@ export default {
category="tertiary"
size="small"
:icon="isExpanded ? 'chevron-lg-up' : 'chevron-lg-down'"
- @click.stop="toggleCollapsed"
+ @click="toggleCollapsed"
/>
</div>
</div>
diff --git a/app/assets/javascripts/reports/grouped_test_report/components/modal.vue b/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
deleted file mode 100644
index ca518aea743..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/components/modal.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
-import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
-
-import CodeBlock from '~/vue_shared/components/code_block.vue';
-import { fieldTypes } from '../../constants';
-
-export default {
- components: {
- CodeBlock,
- GlModal,
- GlLink,
- GlSprintf,
- },
- props: {
- visible: {
- type: Boolean,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- modalData: {
- type: Object,
- required: true,
- },
- },
- computed: {
- filteredModalData() {
- // Filter out the properties that don't have a value
- return Object.fromEntries(
- Object.entries(this.modalData).filter((data) => Boolean(data[1].value)),
- );
- },
- },
- fieldTypes,
-};
-</script>
-<template>
- <gl-modal
- :visible="visible"
- modal-id="modal-mrwidget-reports"
- :title="title"
- :hide-footer="true"
- @hide="$emit('hide')"
- >
- <div v-for="(field, key, index) in filteredModalData" :key="index" class="row gl-mt-3 gl-mb-3">
- <strong class="col-sm-3 text-right"> {{ field.text }}: </strong>
-
- <div class="col-sm-9">
- <code-block v-if="field.type === $options.fieldTypes.codeBlock" :code="field.value" />
-
- <gl-link
- v-else-if="field.type === $options.fieldTypes.link"
- :href="field.value.path"
- target="_blank"
- >
- {{ field.value.text }}
- </gl-link>
-
- <gl-sprintf
- v-else-if="field.type === $options.fieldTypes.seconds"
- :message="__('%{value} s')"
- >
- <template #value>{{ field.value }}</template>
- </gl-sprintf>
-
- <template v-else-if="field.type === $options.fieldTypes.text">
- {{ field.value }}
- </template>
- </div>
- </div>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue b/app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue
deleted file mode 100644
index 8913046d62f..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/components/test_issue_body.vue
+++ /dev/null
@@ -1,64 +0,0 @@
-<script>
-import { GlBadge, GlButton } from '@gitlab/ui';
-import { mapActions } from 'vuex';
-import { sprintf, n__ } from '~/locale';
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
-import { STATUS_NEUTRAL } from '../../constants';
-
-export default {
- name: 'TestIssueBody',
- components: {
- GlBadge,
- GlButton,
- IssueStatusIcon,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- },
- computed: {
- recentFailureMessage() {
- return sprintf(
- n__(
- 'Reports|Failed %{count} time in %{base_branch} in the last 14 days',
- 'Reports|Failed %{count} times in %{base_branch} in the last 14 days',
- this.issue.recent_failures?.count,
- ),
- this.issue.recent_failures,
- );
- },
- showRecentFailures() {
- return this.issue.recent_failures?.count && this.issue.recent_failures?.base_branch;
- },
- status() {
- return this.issue.status || STATUS_NEUTRAL;
- },
- },
- methods: {
- ...mapActions(['openModal']),
- },
-};
-</script>
-<template>
- <div class="gl-display-flex gl-mt-2 gl-mb-2">
- <issue-status-icon :status="status" :status-icon-size="24" class="gl-mr-3" />
- <gl-button
- button-text-classes="gl-white-space-normal! gl-word-break-all gl-text-left"
- variant="link"
- data-testid="test-issue-body-description"
- @click="openModal({ issue })"
- >
- <gl-badge
- v-if="showRecentFailures"
- variant="warning"
- class="gl-mr-2"
- data-testid="test-issue-body-recent-failures"
- >
- {{ recentFailureMessage }}
- </gl-badge>
- {{ issue.name }}
- </gl-button>
- </div>
-</template>
diff --git a/app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue b/app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue
deleted file mode 100644
index be49a03a9a5..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/grouped_test_reports_app.vue
+++ /dev/null
@@ -1,204 +0,0 @@
-<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { mapActions, mapGetters, mapState } from 'vuex';
-import api from '~/api';
-import { sprintf, s__ } from '~/locale';
-import GroupedIssuesList from '../components/grouped_issues_list.vue';
-import { componentNames } from '../components/issue_body';
-import ReportSection from '../components/report_section.vue';
-import SummaryRow from '../components/summary_row.vue';
-import Modal from './components/modal.vue';
-import createStore from './store';
-import {
- summaryTextBuilder,
- reportTextBuilder,
- statusIcon,
- recentFailuresTextBuilder,
-} from './store/utils';
-
-export default {
- name: 'GroupedTestReportsApp',
- store: createStore(),
- components: {
- ReportSection,
- SummaryRow,
- GroupedIssuesList,
- Modal,
- GlButton,
- GlIcon,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- pipelinePath: {
- type: String,
- required: false,
- default: '',
- },
- headBlobPath: {
- type: String,
- required: true,
- },
- },
- componentNames,
- computed: {
- ...mapState(['reports', 'isLoading', 'hasError', 'summary']),
- ...mapState({
- modalTitle: (state) => state.modal.title || '',
- modalData: (state) => state.modal.data || {},
- modalOpen: (state) => state.modal.open || false,
- }),
- ...mapGetters(['summaryStatus']),
- groupedSummaryText() {
- if (this.isLoading) {
- return s__('Reports|Test summary results are being parsed');
- }
-
- if (this.hasError) {
- return s__('Reports|Test summary failed loading results');
- }
-
- return summaryTextBuilder(s__('Reports|Test summary'), this.summary);
- },
- testTabURL() {
- return `${this.pipelinePath}/test_report`;
- },
- showViewFullReport() {
- return this.pipelinePath.length;
- },
- },
- created() {
- this.setPaths({
- endpoint: this.endpoint,
- headBlobPath: this.headBlobPath,
- });
-
- this.fetchReports();
- },
- methods: {
- ...mapActions(['setPaths', 'fetchReports', 'closeModal']),
- handleToggleEvent() {
- api.trackRedisHllUserEvent(this.$options.expandEvent);
- },
- reportText(report) {
- const { name, summary } = report || {};
-
- if (report.status === 'error') {
- return sprintf(s__('Reports|An error occurred while loading %{name} results'), { name });
- }
-
- if (!report.name) {
- return s__('Reports|An error occurred while loading report');
- }
-
- return reportTextBuilder(name, summary);
- },
- hasRecentFailures(summary) {
- return summary?.recentlyFailed > 0;
- },
- recentFailuresText(summary) {
- return recentFailuresTextBuilder(summary);
- },
- getReportIcon(report) {
- return statusIcon(report.status);
- },
- shouldRenderIssuesList(report) {
- return (
- report.existing_failures.length > 0 ||
- report.new_failures.length > 0 ||
- report.resolved_failures.length > 0 ||
- report.existing_errors.length > 0 ||
- report.new_errors.length > 0 ||
- report.resolved_errors.length > 0
- );
- },
- unresolvedIssues(report) {
- return [
- ...report.new_failures,
- ...report.new_errors,
- ...report.existing_failures,
- ...report.existing_errors,
- ];
- },
- resolvedIssues(report) {
- return report.resolved_failures.concat(report.resolved_errors);
- },
- },
- expandEvent: 'i_testing_summary_widget_total',
-};
-</script>
-<template>
- <report-section
- :status="summaryStatus"
- :success-text="groupedSummaryText"
- :loading-text="groupedSummaryText"
- :error-text="groupedSummaryText"
- :has-issues="reports.length > 0"
- :should-emit-toggle-event="true"
- class="mr-widget-section grouped-security-reports mr-report"
- @toggleEvent.once="handleToggleEvent"
- >
- <template v-if="showViewFullReport" #action-buttons>
- <gl-button
- :href="testTabURL"
- target="_blank"
- icon="external-link"
- data-testid="group-test-reports-full-link"
- class="gl-mr-3"
- >
- {{ s__('ciReport|View full report') }}
- </gl-button>
- </template>
- <template v-if="hasRecentFailures(summary)" #sub-heading>
- {{ recentFailuresText(summary) }}
- </template>
- <template #body>
- <div class="mr-widget-grouped-section report-block">
- <template v-for="(report, i) in reports">
- <summary-row
- :key="`summary-row-${i}`"
- :status-icon="getReportIcon(report)"
- nested-summary
- >
- <template #summary>
- <div class="gl-display-inline-flex gl-flex-direction-column">
- <div>{{ reportText(report) }}</div>
- <div v-if="report.suite_errors">
- <div v-if="report.suite_errors.head">
- <gl-icon name="warning" class="gl-mx-2 gl-text-orange-500" />
- {{ s__('Reports|Head report parsing error:') }}
- {{ report.suite_errors.head }}
- </div>
- <div v-if="report.suite_errors.base">
- <gl-icon name="warning" class="gl-mx-2 gl-text-orange-500" />
- {{ s__('Reports|Base report parsing error:') }}
- {{ report.suite_errors.base }}
- </div>
- </div>
- <div v-if="hasRecentFailures(report.summary)">
- {{ recentFailuresText(report.summary) }}
- </div>
- </div>
- </template>
- </summary-row>
- <grouped-issues-list
- v-if="shouldRenderIssuesList(report)"
- :key="`issues-list-${i}`"
- :unresolved-issues="unresolvedIssues(report)"
- :resolved-issues="resolvedIssues(report)"
- :component="$options.componentNames.TestIssueBody"
- :nested-level="2"
- />
- </template>
- <modal
- :visible="modalOpen"
- :title="modalTitle"
- :modal-data="modalData"
- @hide="closeModal"
- />
- </div>
- </template>
- </report-section>
-</template>
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/actions.js b/app/assets/javascripts/reports/grouped_test_report/store/actions.js
deleted file mode 100644
index 73f8df016b6..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/actions.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import Visibility from 'visibilityjs';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import Poll from '~/lib/utils/poll';
-import * as types from './mutation_types';
-
-export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
-
-export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS);
-
-let eTagPoll;
-
-export const clearEtagPoll = () => {
- eTagPoll = null;
-};
-
-export const stopPolling = () => {
- if (eTagPoll) eTagPoll.stop();
-};
-
-export const restartPolling = () => {
- if (eTagPoll) eTagPoll.restart();
-};
-
-/**
- * We need to poll the reports endpoint while they are being parsed in the Backend.
- * This can take up to one minute.
- *
- * Poll.js will handle etag response.
- * While http status code is 204, it means it's parsing, and we'll keep polling
- * When http status code is 200, it means parsing is done, we can show the results & stop polling
- * When http status code is 500, it means parsing went wrong and we stop polling
- */
-export const fetchReports = ({ state, dispatch }) => {
- dispatch('requestReports');
-
- eTagPoll = new Poll({
- resource: {
- getReports(endpoint) {
- return axios.get(endpoint);
- },
- },
- data: state.endpoint,
- method: 'getReports',
- successCallback: ({ data, status }) =>
- dispatch('receiveReportsSuccess', {
- data,
- status,
- }),
- errorCallback: () => dispatch('receiveReportsError'),
- });
-
- if (!Visibility.hidden()) {
- eTagPoll.makeRequest();
- } else {
- axios
- .get(state.endpoint)
- .then(({ data, status }) => dispatch('receiveReportsSuccess', { data, status }))
- .catch(() => dispatch('receiveReportsError'));
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden()) {
- dispatch('restartPolling');
- } else {
- dispatch('stopPolling');
- }
- });
-};
-
-export const receiveReportsSuccess = ({ commit }, response) => {
- // With 204 we keep polling and don't update the state
- if (response.status === httpStatusCodes.OK) {
- commit(types.RECEIVE_REPORTS_SUCCESS, response.data);
- }
-};
-
-export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR);
-
-export const openModal = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload);
-
-export const closeModal = ({ commit }, payload) => commit(types.RESET_ISSUE_MODAL_DATA, payload);
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/getters.js b/app/assets/javascripts/reports/grouped_test_report/store/getters.js
deleted file mode 100644
index e7d1675f74a..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/getters.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
-
-export const summaryStatus = (state) => {
- if (state.isLoading) {
- return LOADING;
- }
-
- if (state.hasError || state.status === STATUS_FAILED) {
- return ERROR;
- }
-
- return SUCCESS;
-};
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/index.js b/app/assets/javascripts/reports/grouped_test_report/store/index.js
deleted file mode 100644
index a2edfa94a48..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const getStoreConfig = () => ({
- actions,
- mutations,
- getters,
- state: state(),
-});
-
-export default () => new Vuex.Store(getStoreConfig());
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js b/app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js
deleted file mode 100644
index ff839c564b6..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/mutation_types.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export const SET_PATHS = 'SET_PATHS';
-
-export const REQUEST_REPORTS = 'REQUEST_REPORTS';
-export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
-export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
-export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA';
-export const RESET_ISSUE_MODAL_DATA = 'RESET_ISSUE_MODAL_DATA';
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/mutations.js b/app/assets/javascripts/reports/grouped_test_report/store/mutations.js
deleted file mode 100644
index 2b88776815b..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/mutations.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import * as types from './mutation_types';
-import { countRecentlyFailedTests, formatFilePath } from './utils';
-
-export default {
- [types.SET_PATHS](state, { endpoint, headBlobPath }) {
- state.endpoint = endpoint;
- state.headBlobPath = headBlobPath;
- },
- [types.REQUEST_REPORTS](state) {
- state.isLoading = true;
- },
- [types.RECEIVE_REPORTS_SUCCESS](state, response) {
- state.hasError = response.suites.some((suite) => suite.status === 'error');
-
- state.isLoading = false;
-
- state.summary.total = response.summary.total;
- state.summary.resolved = response.summary.resolved;
- state.summary.failed = response.summary.failed;
- state.summary.errored = response.summary.errored;
- state.summary.recentlyFailed = countRecentlyFailedTests(response.suites);
-
- state.status = response.status;
- state.reports = response.suites;
-
- state.reports.forEach((report, i) => {
- if (!state.reports[i].summary) return;
- state.reports[i].summary.recentlyFailed = countRecentlyFailedTests(report);
- });
- },
- [types.RECEIVE_REPORTS_ERROR](state) {
- state.isLoading = false;
- state.hasError = true;
-
- state.reports = [];
- state.summary = {
- total: 0,
- resolved: 0,
- failed: 0,
- errored: 0,
- recentlyFailed: 0,
- };
- state.status = null;
- },
- [types.SET_ISSUE_MODAL_DATA](state, payload) {
- const { issue } = payload;
- state.modal.title = issue.name;
-
- Object.keys(issue).forEach((key) => {
- if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) {
- state.modal.data[key] = {
- ...state.modal.data[key],
- value: issue[key],
- };
- }
- });
-
- if (issue.file) {
- state.modal.data.filename.value = {
- text: issue.file,
- path: `${state.headBlobPath}/${formatFilePath(issue.file)}`,
- };
- }
-
- state.modal.open = true;
- },
- [types.RESET_ISSUE_MODAL_DATA](state) {
- state.modal.open = false;
-
- // Resetting modal data
- state.modal.title = null;
- Object.keys(state.modal.data).forEach((key) => {
- state.modal.data[key] = {
- ...state.modal.data[key],
- value: null,
- };
- });
- },
-};
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/state.js b/app/assets/javascripts/reports/grouped_test_report/store/state.js
deleted file mode 100644
index 46909bde337..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/state.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { s__ } from '~/locale';
-import { fieldTypes } from '../../constants';
-
-export default () => ({
- endpoint: null,
-
- isLoading: false,
- hasError: false,
-
- status: null,
-
- summary: {
- total: 0,
- resolved: 0,
- failed: 0,
- errored: 0,
- },
-
- /**
- * Each report will have the following format:
- * {
- * name: {String},
- * summary: {
- * total: {Number},
- * resolved: {Number},
- * failed: {Number},
- * errored: {Number},
- * },
- * new_failures: {Array.<Object>},
- * resolved_failures: {Array.<Object>},
- * existing_failures: {Array.<Object>},
- * new_errors: {Array.<Object>},
- * resolved_errors: {Array.<Object>},
- * existing_errors: {Array.<Object>},
- * }
- */
- reports: [],
-
- modal: {
- title: null,
- open: false,
-
- data: {
- classname: {
- value: null,
- text: s__('Reports|Classname'),
- type: fieldTypes.text,
- },
- filename: {
- value: null,
- text: s__('Reports|Filename'),
- type: fieldTypes.link,
- },
- execution_time: {
- value: null,
- text: s__('Reports|Execution time'),
- type: fieldTypes.seconds,
- },
- failure: {
- value: null,
- text: s__('Reports|Failure'),
- type: fieldTypes.codeBlock,
- },
- system_output: {
- value: null,
- text: s__('Reports|System output'),
- type: fieldTypes.codeBlock,
- },
- },
- },
-});
diff --git a/app/assets/javascripts/reports/grouped_test_report/store/utils.js b/app/assets/javascripts/reports/grouped_test_report/store/utils.js
deleted file mode 100644
index df5dd73b66c..00000000000
--- a/app/assets/javascripts/reports/grouped_test_report/store/utils.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import { sprintf, n__, s__, __ } from '~/locale';
-import {
- STATUS_FAILED,
- STATUS_SUCCESS,
- ICON_WARNING,
- ICON_SUCCESS,
- ICON_NOTFOUND,
-} from '../../constants';
-
-const textBuilder = (results) => {
- const { failed, errored, resolved, total } = results;
-
- const failedOrErrored = (failed || 0) + (errored || 0);
- const failedString = failed ? n__('%d failed', '%d failed', failed) : null;
- const erroredString = errored ? n__('%d error', '%d errors', errored) : null;
- const combinedString =
- failed && errored ? `${failedString}, ${erroredString}` : failedString || erroredString;
- const resolvedString = resolved
- ? n__('%d fixed test result', '%d fixed test results', resolved)
- : null;
- const totalString = total ? n__('out of %d total test', 'out of %d total tests', total) : null;
-
- let resultsString = s__('Reports|no changed test results');
-
- if (failedOrErrored) {
- if (resolved) {
- resultsString = sprintf(s__('Reports|%{combinedString} and %{resolvedString}'), {
- combinedString,
- resolvedString,
- });
- } else {
- resultsString = combinedString;
- }
- } else if (resolved) {
- resultsString = resolvedString;
- }
-
- return `${resultsString} ${totalString}`;
-};
-
-export const summaryTextBuilder = (name = '', results = {}) => {
- const resultsString = textBuilder(results);
- return sprintf(__('%{name} contained %{resultsString}'), { name, resultsString });
-};
-
-export const reportTextBuilder = (name = '', results = {}) => {
- const resultsString = textBuilder(results);
- return sprintf(__('%{name} found %{resultsString}'), { name, resultsString });
-};
-
-export const recentFailuresTextBuilder = (summary = {}) => {
- const { failed, recentlyFailed } = summary;
- if (!failed || !recentlyFailed) return '';
-
- if (failed < 2) {
- return sprintf(
- s__(
- 'Reports|%{recentlyFailed} out of %{failed} failed test has failed more than once in the last 14 days',
- ),
- { recentlyFailed, failed },
- );
- }
- return sprintf(
- n__(
- 'Reports|%{recentlyFailed} out of %{failed} failed tests has failed more than once in the last 14 days',
- 'Reports|%{recentlyFailed} out of %{failed} failed tests have failed more than once in the last 14 days',
- recentlyFailed,
- ),
- { recentlyFailed, failed },
- );
-};
-
-export const countRecentlyFailedTests = (subject) => {
- // handle either a single report or an array of reports
- const reports = !subject.length ? [subject] : subject;
-
- return reports
- .map((report) => {
- return (
- [report.new_failures, report.existing_failures, report.resolved_failures]
- // only count tests which have failed more than once
- .map(
- (failureArray) =>
- failureArray.filter((failure) => failure.recent_failures?.count > 1).length,
- )
- .reduce((total, count) => total + count, 0)
- );
- })
- .reduce((total, count) => total + count, 0);
-};
-
-export const statusIcon = (status) => {
- if (status === STATUS_FAILED) {
- return ICON_WARNING;
- }
-
- if (status === STATUS_SUCCESS) {
- return ICON_SUCCESS;
- }
-
- return ICON_NOTFOUND;
-};
-
-/**
- * Removes `./` from the beginning of a file path so it can be appended onto a blob path
- * @param {String} file
- * @returns {String} - formatted value
- */
-export const formatFilePath = (file) => {
- return file.replace(/^\.?\/*/, '');
-};
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index bf1667d8734..101625a4b72 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -14,7 +14,7 @@ import WebIdeLink from '~/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';
-import addBlameLink from '~/blob/blob_blame_link';
+import { addBlameLink } from '~/blob/blob_blame_link';
import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
import userInfoQuery from '../queries/user_info.query.graphql';
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 77d3a517d28..3a6d7d2f779 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -93,13 +93,9 @@ export const LFS_STORAGE = 'lfs';
* These are file types that we want the legacy (backend) syntax highlighter to highlight.
*/
export const LEGACY_FILE_TYPES = [
- 'gemfile',
- 'composer_json',
'podfile',
'podspec',
- 'podspec_json',
'cartfile',
- 'godeps_json',
'requirements_txt',
'cargo_toml',
'go_mod',
diff --git a/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue b/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue
deleted file mode 100644
index e3a9a9fd8a4..00000000000
--- a/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue
+++ /dev/null
@@ -1,58 +0,0 @@
-<script>
-import allChangesCommittedSvg from '@gitlab/svgs/dist/illustrations/multi-editor_all_changes_committed_empty.svg';
-import { GlBanner } from '@gitlab/ui';
-
-import { s__ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-const I18N_TITLE = s__("Runners|We've made some changes and want your feedback");
-const I18N_DESCRIPTION = s__(
- "Runners|We want you to be able to manage your runners easily and efficiently from this page, and we are making changes to get there. Give us feedback on how we're doing!",
-);
-const I18N_LINK = s__('Runners|Add your feedback in the issue');
-
-// use a data url instead getting it from via HTML data-* attributes to simplify removal of this feature flag
-const ILLUSTRATION_URL = `data:image/svg+xml;utf8,${encodeURIComponent(allChangesCommittedSvg)}`;
-const ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/371621';
-const STORAGE_KEY = 'runner_list_stacked_layout_feedback_dismissed';
-
-export default {
- components: {
- GlBanner,
- LocalStorageSync,
- },
- data() {
- return {
- isDismissed: false,
- };
- },
- methods: {
- onClose() {
- this.isDismissed = true;
- },
- },
- I18N_TITLE,
- I18N_DESCRIPTION,
- I18N_LINK,
- ILLUSTRATION_URL,
- ISSUE_URL,
- STORAGE_KEY,
-};
-</script>
-
-<template>
- <div>
- <local-storage-sync v-model="isDismissed" :storage-key="$options.STORAGE_KEY" />
- <gl-banner
- v-if="!isDismissed"
- :svg-path="$options.ILLUSTRATION_URL"
- :title="$options.I18N_TITLE"
- :button-text="$options.I18N_LINK"
- :button-link="$options.ISSUE_URL"
- class="gl-my-5"
- @close="onClose"
- >
- <p>{{ $options.I18N_DESCRIPTION }}</p>
- </gl-banner>
- </div>
-</template>
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 789efc8f09d..6f29864c0a2 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -1,48 +1,29 @@
<script>
-import { GlButton, GlLink } from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
-import ConfidentialityFilter from './confidentiality_filter.vue';
-import StatusFilter from './status_filter.vue';
+import { mapState } from 'vuex';
+import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS } from '../constants';
+import ResultsFilters from './results_filters.vue';
export default {
name: 'GlobalSearchSidebar',
components: {
- GlButton,
- GlLink,
- StatusFilter,
- ConfidentialityFilter,
+ ResultsFilters,
+ ScopeNavigation,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['urlQuery', 'sidebarDirty']),
- showReset() {
- return this.urlQuery.state || this.urlQuery.confidential;
+ ...mapState(['urlQuery']),
+ showFilters() {
+ return this.urlQuery.scope === SCOPE_ISSUES || this.urlQuery.scope === SCOPE_MERGE_REQUESTS;
},
- showSidebar() {
- return this.urlQuery.scope === 'issues' || this.urlQuery.scope === 'merge_requests';
- },
- },
- methods: {
- ...mapActions(['applyQuery', 'resetQuery']),
},
};
</script>
<template>
- <form
- class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5"
- @submit.prevent="applyQuery"
- >
- <template v-if="showSidebar">
- <status-filter />
- <confidentiality-filter />
- <div class="gl-display-flex gl-align-items-center gl-mt-3">
- <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
- {{ __('Apply') }}
- </gl-button>
- <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
- __('Reset filters')
- }}</gl-link>
- </div>
- </template>
- </form>
+ <section class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5">
+ <scope-navigation v-if="glFeatures.searchPageVerticalNav" />
+ <results-filters v-if="showFilters" />
+ </section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue
new file mode 100644
index 00000000000..5b53f94bb53
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlButton, GlLink } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ConfidentialityFilter from './confidentiality_filter.vue';
+import StatusFilter from './status_filter.vue';
+
+export default {
+ name: 'ResultsFilters',
+ components: {
+ GlButton,
+ GlLink,
+ StatusFilter,
+ ConfidentialityFilter,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ computed: {
+ ...mapState(['urlQuery', 'sidebarDirty']),
+ showReset() {
+ return this.urlQuery.state || this.urlQuery.confidential;
+ },
+ searchPageVerticalNavFeatureFlag() {
+ return this.glFeatures.searchPageVerticalNav;
+ },
+ },
+ methods: {
+ ...mapActions(['applyQuery', 'resetQuery']),
+ },
+};
+</script>
+
+<template>
+ <form
+ :class="searchPageVerticalNavFeatureFlag ? 'gl-px-5' : 'gl-px-0'"
+ @submit.prevent="applyQuery"
+ >
+ <hr v-if="searchPageVerticalNavFeatureFlag" class="gl-my-5 gl-border-gray-100" />
+ <status-filter />
+ <confidentiality-filter />
+ <div class="gl-display-flex gl-align-items-center gl-mt-4">
+ <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
+ {{ __('Apply') }}
+ </gl-button>
+ <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
+ __('Reset filters')
+ }}</gl-link>
+ </div>
+ </form>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
new file mode 100644
index 00000000000..f5e1525090e
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlNav, GlNavItem } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import { formatNumber } from '~/locale';
+import Tracking from '~/tracking';
+import { NAV_LINK_DEFAULT_CLASSES, NUMBER_FORMATING_OPTIONS } from '../constants';
+
+export default {
+ name: 'ScopeNavigation',
+ components: {
+ GlNav,
+ GlNavItem,
+ },
+ mixins: [Tracking.mixin()],
+ computed: {
+ ...mapState(['navigation', 'urlQuery']),
+ },
+ created() {
+ this.fetchSidebarCount();
+ },
+ methods: {
+ ...mapActions(['fetchSidebarCount']),
+ activeClasses(currentScope) {
+ return currentScope === this.urlQuery.scope ? 'gl-font-weight-bold' : '';
+ },
+ showFormatedCount(count) {
+ if (!count) {
+ return '0';
+ }
+ const countNumber = parseInt(count.replace(/,/g, ''), 10);
+ return formatNumber(countNumber, NUMBER_FORMATING_OPTIONS);
+ },
+ handleClick(scope) {
+ this.track('click_menu_item', { label: `vertical_navigation_${scope}` });
+ },
+ linkClasses(scope) {
+ return [
+ { 'gl-font-weight-bold': scope === this.urlQuery.scope },
+ ...this.$options.NAV_LINK_DEFAULT_CLASSES,
+ ];
+ },
+ },
+ NAV_LINK_DEFAULT_CLASSES,
+};
+</script>
+
+<template>
+ <nav data-testid="search-filter">
+ <gl-nav vertical pills>
+ <gl-nav-item
+ v-for="(item, scope, index) in navigation"
+ :key="scope"
+ :link-classes="linkClasses(scope)"
+ class="gl-mb-1"
+ :href="item.link"
+ :active="urlQuery.scope ? urlQuery.scope === scope : index === 0"
+ @click="handleClick(scope)"
+ ><span>{{ item.label }}</span
+ ><span v-if="item.count" class="gl-font-sm gl-font-weight-normal">
+ {{ showFormatedCount(item.count) }}
+ </span>
+ </gl-nav-item>
+ </gl-nav>
+ <hr class="gl-mt-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" />
+ </nav>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
new file mode 100644
index 00000000000..3621138afe4
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -0,0 +1,11 @@
+export const SCOPE_ISSUES = 'issues';
+export const SCOPE_MERGE_REQUESTS = 'merge_requests';
+
+export const NUMBER_FORMATING_OPTIONS = { notation: 'compact', compactDisplay: 'short' };
+export const NAV_LINK_DEFAULT_CLASSES = [
+ 'gl-display-flex',
+ 'gl-flex-direction-row',
+ 'gl-flex-wrap-nowrap',
+ 'gl-justify-content-space-between',
+ 'gl-text-gray-900',
+];
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index be5742e5949..2a1b744561d 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,6 +1,8 @@
import Api from '~/api';
import { createAlert } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants';
import * as types from './mutation_types';
@@ -99,3 +101,19 @@ export const applyQuery = ({ state }) => {
export const resetQuery = ({ state }) => {
visitUrl(setUrlParams({ ...state.query, page: null, state: null, confidential: null }));
};
+
+export const fetchSidebarCount = ({ commit, state }) => {
+ const promises = Object.keys(state.navigation).map((scope) => {
+ // active nav item has count already so we skip it
+ if (scope !== state.urlQuery.scope) {
+ return axios
+ .get(state.navigation[scope].count_link)
+ .then(({ data: { count } }) => {
+ commit(types.RECEIVE_NAVIGATION_COUNT, { key: scope, count });
+ })
+ .catch((e) => logError(e));
+ }
+ return Promise.resolve();
+ });
+ return Promise.all(promises);
+};
diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js
index 4fa88822722..e20a43808cf 100644
--- a/app/assets/javascripts/search/store/index.js
+++ b/app/assets/javascripts/search/store/index.js
@@ -7,11 +7,11 @@ import createState from './state';
Vue.use(Vuex);
-export const getStoreConfig = ({ query }) => ({
+export const getStoreConfig = ({ query, navigation }) => ({
actions,
getters,
mutations,
- state: createState({ query }),
+ state: createState({ query, navigation }),
});
const createStore = (config) => new Vuex.Store(getStoreConfig(config));
diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js
index bf1e3e79cba..511b93cad2b 100644
--- a/app/assets/javascripts/search/store/mutation_types.js
+++ b/app/assets/javascripts/search/store/mutation_types.js
@@ -10,3 +10,4 @@ export const SET_QUERY = 'SET_QUERY';
export const SET_SIDEBAR_DIRTY = 'SET_SIDEBAR_DIRTY';
export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS';
+export const RECEIVE_NAVIGATION_COUNT = 'RECEIVE_NAVIGATION_COUNT';
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index 5d154fe3aa0..c1339845272 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -32,4 +32,8 @@ export default {
[types.LOAD_FREQUENT_ITEMS](state, { key, data }) {
state.frequentItems[key] = data;
},
+ [types.RECEIVE_NAVIGATION_COUNT](state, { key, count }) {
+ const item = { ...state.navigation[key], count };
+ state.navigation = { ...state.navigation, [key]: item };
+ },
};
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index d4005697f35..b64231a8688 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 }) => ({
+const createState = ({ query, navigation }) => ({
urlQuery: cloneDeep(query),
query,
groups: [],
@@ -13,5 +13,6 @@ const createState = ({ query }) => ({
[PROJECTS_LOCAL_STORAGE_KEY]: [],
},
sidebarDirty: false,
+ navigation,
});
export default createState;
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index b14e816a674..ffba3aac681 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -29,7 +29,7 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
formLabels: {
- createProject: __('Self monitoring'),
+ createProject: __('Self-monitoring'),
},
data() {
return {
@@ -60,7 +60,7 @@ export default {
if (this.projectCreated) {
return sprintf(
s__(
- 'SelfMonitoring|Self monitoring is active. Use the %{projectLinkStart}self monitoring project%{projectLinkEnd} to monitor the health of your instance.',
+ 'SelfMonitoring|Self-monitoring is active. Use the %{projectLinkStart}self-monitoring project%{projectLinkEnd} to monitor the health of your instance.',
),
{
projectLinkStart: `<a href="${this.selfMonitorProjectFullUrl}">`,
@@ -71,7 +71,7 @@ export default {
}
return s__(
- 'SelfMonitoring|Activate self monitoring to create a project to use to monitor the health of your instance.',
+ 'SelfMonitoring|Activate self-monitoring to create a project to use to monitor the health of your instance.',
);
},
helpDocsPath() {
@@ -139,11 +139,11 @@ export default {
<h4
class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only"
>
- {{ s__('SelfMonitoring|Self monitoring') }}
+ {{ s__('SelfMonitoring|Self-monitoring') }}
</h4>
<gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button>
<p class="js-section-sub-header">
- {{ s__('SelfMonitoring|Activate or deactivate instance self monitoring.') }}
+ {{ s__('SelfMonitoring|Activate or deactivate instance self-monitoring.') }}
<gl-link :href="helpDocsPath">{{ __('Learn more.') }}</gl-link>
</p>
</div>
@@ -160,9 +160,9 @@ export default {
</form>
</div>
<gl-modal
- :title="s__('SelfMonitoring|Deactivate self monitoring?')"
+ :title="s__('SelfMonitoring|Deactivate self-monitoring?')"
:modal-id="modalId"
- :ok-title="__('Delete self monitoring project')"
+ :ok-title="__('Delete self-monitoring project')"
:cancel-title="__('Cancel')"
ok-variant="danger"
category="primary"
@@ -172,7 +172,7 @@ export default {
<div>
{{
s__(
- 'SelfMonitoring|Deactivating self monitoring deletes the self monitoring project. Are you sure you want to deactivate self monitoring and delete the project?',
+ 'SelfMonitoring|Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project?',
)
}}
</div>
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
index f37b654b00a..5b9e994290c 100644
--- a/app/assets/javascripts/self_monitor/store/actions.js
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -56,7 +56,7 @@ export const requestCreateProjectSuccess = ({ commit, dispatch }, selfMonitorDat
commit(types.SET_LOADING, false);
commit(types.SET_PROJECT_URL, selfMonitorData.project_full_path);
commit(types.SET_ALERT_CONTENT, {
- message: s__('SelfMonitoring|Self monitoring project successfully created.'),
+ message: s__('SelfMonitoring|Self-monitoring project successfully created.'),
actionText: __('View project'),
actionName: 'viewSelfMonitorProject',
});
@@ -108,7 +108,7 @@ export const requestDeleteProjectSuccess = ({ commit }) => {
commit(types.SET_PROJECT_URL, '');
commit(types.SET_PROJECT_CREATED, false);
commit(types.SET_ALERT_CONTENT, {
- message: s__('SelfMonitoring|Self monitoring project successfully deleted.'),
+ message: s__('SelfMonitoring|Self-monitoring project successfully deleted.'),
actionText: __('Undo'),
actionName: 'createProject',
});
diff --git a/app/assets/javascripts/sentry/constants.js b/app/assets/javascripts/sentry/constants.js
new file mode 100644
index 00000000000..fd96da5faf6
--- /dev/null
+++ b/app/assets/javascripts/sentry/constants.js
@@ -0,0 +1,43 @@
+import { __ } from '~/locale';
+
+export const IGNORE_ERRORS = [
+ // Random plugins/extensions
+ 'top.GLOBALS',
+ // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
+ 'originalCreateNotification',
+ 'canvas.contentDocument',
+ 'MyApp_RemoveAllHighlights',
+ 'http://tt.epicplay.com',
+ __("Can't find variable: ZiteReader"),
+ __('jigsaw is not defined'),
+ __('ComboSearch is not defined'),
+ 'http://loading.retry.widdit.com/',
+ 'atomicFindClose',
+ // Facebook borked
+ 'fb_xd_fragment',
+ // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
+ // reduce this. (thanks @acdha)
+ 'bmi_SafeAddOnload',
+ 'EBCallBackMessageReceived',
+ // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
+ 'conduitPage',
+];
+
+export const DENY_URLS = [
+ // Facebook flakiness
+ /graph\.facebook\.com/i,
+ // Facebook blocked
+ /connect\.facebook\.net\/en_US\/all\.js/i,
+ // Woopra flakiness
+ /eatdifferent\.com\.woopra-ns\.com/i,
+ /static\.woopra\.com\/js\/woopra\.js/i,
+ // Chrome extensions
+ /extensions\//i,
+ /^chrome:\/\//i,
+ // Other plugins
+ /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
+ /webappstoolbarba\.texthelp\.com\//i,
+ /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
+];
+
+export const SAMPLE_RATE = 0.95;
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index 8f3c4c644bf..4c5b8dbad5a 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -1,52 +1,11 @@
import * as Sentry from '@sentry/browser';
import $ from 'jquery';
import { __ } from '~/locale';
-
-const IGNORE_ERRORS = [
- // Random plugins/extensions
- 'top.GLOBALS',
- // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
- 'originalCreateNotification',
- 'canvas.contentDocument',
- 'MyApp_RemoveAllHighlights',
- 'http://tt.epicplay.com',
- __("Can't find variable: ZiteReader"),
- __('jigsaw is not defined'),
- __('ComboSearch is not defined'),
- 'http://loading.retry.widdit.com/',
- 'atomicFindClose',
- // Facebook borked
- 'fb_xd_fragment',
- // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
- // reduce this. (thanks @acdha)
- 'bmi_SafeAddOnload',
- 'EBCallBackMessageReceived',
- // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
- 'conduitPage',
-];
-
-const BLACKLIST_URLS = [
- // Facebook flakiness
- /graph\.facebook\.com/i,
- // Facebook blocked
- /connect\.facebook\.net\/en_US\/all\.js/i,
- // Woopra flakiness
- /eatdifferent\.com\.woopra-ns\.com/i,
- /static\.woopra\.com\/js\/woopra\.js/i,
- // Chrome extensions
- /extensions\//i,
- /^chrome:\/\//i,
- // Other plugins
- /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
- /webappstoolbarba\.texthelp\.com\//i,
- /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
-];
-
-const SAMPLE_RATE = 0.95;
+import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants';
const SentryConfig = {
IGNORE_ERRORS,
- BLACKLIST_URLS,
+ BLACKLIST_URLS: DENY_URLS,
SAMPLE_RATE,
init(options = {}) {
this.options = options;
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index 6e18cf36690..2a9100f0cb5 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -55,6 +55,7 @@ export default {
class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-test-id="edit-link"
+ data-qa-selector="edit_link"
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index 29ea390a81d..cf07752a0b8 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -56,6 +56,7 @@ export default {
type="button"
class="gl-button btn-link gl-reset-color!"
data-testid="assign-yourself"
+ data-qa-selector="assign_yourself_button"
@click="assignSelf"
>
{{ __('assign yourself') }}
diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
index 0e4d4c74160..d83ae782e26 100644
--- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue
@@ -91,6 +91,7 @@ export default {
<div
class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center"
data-testid="username"
+ data-qa-selector="username"
>
<user-name-with-status :name="user.name" :availability="userAvailability(user)" />
</div>
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 98468583992..c262d65f6ce 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -170,7 +170,7 @@ export default {
this.$emit('closeForm');
},
openDatePicker() {
- this.$refs.datePicker.calendar.show();
+ this.$refs.datePicker.show();
},
setFixedDate(isFixed) {
const date = this.issuable[dateFields[this.dateType].dateFixed];
diff --git a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
new file mode 100644
index 00000000000..1fff089eab4
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue
@@ -0,0 +1,115 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { TYPE_MILESTONE } from '~/graphql_shared/constants';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { __ } from '~/locale';
+import { IssuableAttributeType } from '../../constants';
+import SidebarDropdown from '../sidebar_dropdown.vue';
+
+const noMilestone = {
+ id: 0,
+ title: __('No milestone'),
+};
+
+const placeholderMilestone = {
+ id: -1,
+ title: __('Select milestone'),
+};
+
+export default {
+ issuableAttribute: IssuableAttributeType.Milestone,
+ components: {
+ GlDropdownItem,
+ SidebarDropdown,
+ },
+ props: {
+ attrWorkspacePath: {
+ required: true,
+ type: String,
+ },
+ canAdminMilestone: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
+ },
+ },
+ inputName: {
+ type: String,
+ required: false,
+ default: 'update[milestone_id]',
+ },
+ milestoneId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ milestoneTitle: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ projectMilestonesPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ workspaceType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return [WorkspaceType.group, WorkspaceType.project].includes(value);
+ },
+ },
+ },
+ data() {
+ return {
+ milestone: this.milestoneId
+ ? { id: convertToGraphQLId(TYPE_MILESTONE, this.milestoneId), title: this.milestoneTitle }
+ : placeholderMilestone,
+ };
+ },
+ computed: {
+ footerItemText() {
+ return this.canAdminMilestone ? __('Manage milestones') : __('View milestones');
+ },
+ value() {
+ return this.milestone.id === placeholderMilestone.id
+ ? undefined
+ : getIdFromGraphQLId(this.milestone.id);
+ },
+ },
+ methods: {
+ handleChange(milestone) {
+ this.milestone = milestone.id === null ? noMilestone : milestone;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input type="hidden" :name="inputName" :value="value" />
+ <sidebar-dropdown
+ :attr-workspace-path="attrWorkspacePath"
+ :current-attribute="milestone"
+ :issuable-attribute="$options.issuableAttribute"
+ :issuable-type="issuableType"
+ :workspace-type="workspaceType"
+ data-qa-selector="issuable_milestone_dropdown"
+ @change="handleChange"
+ >
+ <template #footer>
+ <gl-dropdown-item v-if="projectMilestonesPath" :href="projectMilestonesPath">
+ {{ footerItemText }}
+ </gl-dropdown-item>
+ </template>
+ </sidebar-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index ad061dd2e6b..5f1350690eb 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -9,6 +9,8 @@ import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql';
+import mergeRequestReviewersUpdatedSubscription from '~/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
@@ -66,6 +68,36 @@ export default {
error() {
createAlert({ message: __('An error occurred while fetching reviewers.') });
},
+ subscribeToMore: {
+ document() {
+ return mergeRequestReviewersUpdatedSubscription;
+ },
+ variables() {
+ return {
+ issuableId: this.issuable?.id,
+ };
+ },
+ skip() {
+ return !this.issuable?.id || !this.isRealtimeEnabled;
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestReviewersUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestReviewersUpdated) {
+ this.store.setReviewersFromRealtime(
+ mergeRequestReviewersUpdated.reviewers.nodes.map((r) => ({
+ ...r,
+ id: getIdFromGraphQLId(r.id),
+ })),
+ );
+ }
+ },
+ },
},
},
data() {
@@ -87,6 +119,9 @@ export default {
canUpdate() {
return this.issuable.userPermissions?.adminMergeRequest || false;
},
+ isRealtimeEnabled() {
+ return this.glFeatures.realtimeReviewers;
+ },
},
created() {
this.store = new Store();
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue
new file mode 100644
index 00000000000..a135dfdca72
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue
@@ -0,0 +1,34 @@
+<script>
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { state } from './sidebar_reviewers.vue';
+
+export default {
+ data() {
+ return state;
+ },
+ computed: {
+ reviewers() {
+ return this.issuable?.reviewers?.nodes || [];
+ },
+ },
+ methods: {
+ getIdFromGraphQLId,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input
+ v-for="reviewer in reviewers"
+ :key="reviewer.id"
+ type="hidden"
+ name="merge_request[reviewer_ids][]"
+ :value="getIdFromGraphQLId(reviewer.id)"
+ :data-avatar-url="reviewer.avatarUrl"
+ :data-name="reviewer.name"
+ :data-username="reviewer.username"
+ :data-can-merge="reviewer.mergeRequestInteraction.canMerge"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
new file mode 100644
index 00000000000..26e2bc96f54
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
@@ -0,0 +1,252 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlDropdownText,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { kebabCase, snakeCase } from 'lodash';
+import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { __ } from '~/locale';
+import {
+ defaultEpicSort,
+ dropdowni18nText,
+ epicIidPattern,
+ issuableAttributesQueries,
+ IssuableAttributeState,
+ IssuableAttributeType,
+ IssuableAttributeTypeKeyMap,
+ LocalizedIssuableAttributeType,
+ noAttributeId,
+} from 'ee_else_ce/sidebar/constants';
+import { createAlert } from '~/flash';
+import { PathIdSeparator } from '~/related_issues/constants';
+
+export default {
+ noAttributeId,
+ i18n: {
+ expired: __('(expired)'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ },
+ inject: {
+ issuableAttributesQueries: {
+ default: issuableAttributesQueries,
+ },
+ issuableAttributesState: {
+ default: IssuableAttributeState,
+ },
+ widgetTitleText: {
+ default: {
+ [IssuableAttributeType.Milestone]: __('Milestone'),
+ expired: __('(expired)'),
+ none: __('None'),
+ },
+ },
+ },
+ props: {
+ attrWorkspacePath: {
+ required: true,
+ type: String,
+ },
+ currentAttribute: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ issuableAttribute: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
+ },
+ },
+ workspaceType: {
+ type: String,
+ required: false,
+ default: WorkspaceType.project,
+ validator(value) {
+ return [WorkspaceType.group, WorkspaceType.project].includes(value);
+ },
+ },
+ },
+ data() {
+ return {
+ attributesList: [],
+ searchTerm: '',
+ skipQuery: true,
+ };
+ },
+ apollo: {
+ attributesList: {
+ query() {
+ const { list } = this.issuableAttributeQuery;
+ const { query } = list[this.issuableType];
+ return query[this.workspaceType] || query;
+ },
+ variables() {
+ if (!this.isEpic) {
+ return {
+ fullPath: this.attrWorkspacePath,
+ title: this.searchTerm,
+ state: this.issuableAttributesState[this.issuableAttribute],
+ };
+ }
+
+ const variables = {
+ fullPath: this.attrWorkspacePath,
+ state: this.issuableAttributesState[this.issuableAttribute],
+ sort: defaultEpicSort,
+ };
+
+ if (epicIidPattern.test(this.searchTerm)) {
+ const matches = this.searchTerm.match(epicIidPattern);
+ variables.iidStartsWith = matches.groups.iid;
+ } else if (this.searchTerm !== '') {
+ variables.in = 'TITLE';
+ variables.title = this.searchTerm;
+ }
+
+ return variables;
+ },
+ update: (data) => data?.workspace?.attributes?.nodes ?? [],
+ error(error) {
+ createAlert({ message: this.i18n.listFetchError, captureError: true, error });
+ },
+ skip() {
+ if (
+ this.isEpic &&
+ this.searchTerm.startsWith(PathIdSeparator.Epic) &&
+ this.searchTerm.length < 2
+ ) {
+ return true;
+ }
+ return this.skipQuery;
+ },
+ debounce: 250,
+ },
+ },
+ computed: {
+ attributeTypeTitle() {
+ return this.widgetTitleText[this.issuableAttribute];
+ },
+ dropdownText() {
+ return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle;
+ },
+ emptyPropsList() {
+ return this.attributesList.length === 0;
+ },
+ i18n() {
+ const localizedAttribute =
+ LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]];
+ return dropdowni18nText(localizedAttribute, this.issuableType);
+ },
+ isEpic() {
+ // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
+ return this.issuableAttribute === IssuableType.Epic;
+ },
+ issuableAttributeQuery() {
+ return this.issuableAttributesQueries[this.issuableAttribute];
+ },
+ formatIssuableAttribute() {
+ return {
+ kebab: kebabCase(this.issuableAttribute),
+ snake: snakeCase(this.issuableAttribute),
+ };
+ },
+ },
+ methods: {
+ isAttributeChecked(attributeId) {
+ return (
+ attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId)
+ );
+ },
+ isAttributeOverdue(attribute) {
+ return this.issuableAttribute === IssuableAttributeType.Milestone
+ ? attribute?.expired
+ : false;
+ },
+ handleShow() {
+ this.skipQuery = false;
+ },
+ setFocus() {
+ this.$refs.search.focusInput();
+ },
+ show() {
+ this.$refs.dropdown.show();
+ },
+ updateAttribute(attribute) {
+ this.$emit('change', attribute);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ ref="dropdown"
+ block
+ :header-text="i18n.assignAttribute"
+ lazy
+ :text="dropdownText"
+ toggle-class="gl-m-0"
+ @show="handleShow"
+ @shown="setFocus"
+ >
+ <gl-search-box-by-type ref="search" v-model="searchTerm" :placeholder="__('Search')" />
+ <gl-dropdown-item
+ :data-testid="`no-${formatIssuableAttribute.kebab}-item`"
+ is-check-item
+ :is-checked="isAttributeChecked($options.noAttributeId)"
+ @click="$emit('change', { id: $options.noAttributeId })"
+ >
+ {{ i18n.noAttribute }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-loading-icon
+ v-if="$apollo.queries.attributesList.loading"
+ size="sm"
+ class="gl-py-4"
+ data-testid="loading-icon-dropdown"
+ />
+ <template v-else>
+ <gl-dropdown-text v-if="emptyPropsList">
+ {{ i18n.noAttributesFound }}
+ </gl-dropdown-text>
+ <slot
+ v-else
+ name="list"
+ :attributes-list="attributesList"
+ :is-attribute-checked="isAttributeChecked"
+ :update-attribute="updateAttribute"
+ >
+ <gl-dropdown-item
+ v-for="attrItem in attributesList"
+ :key="attrItem.id"
+ is-check-item
+ :is-checked="isAttributeChecked(attrItem.id)"
+ :data-testid="`${formatIssuableAttribute.kebab}-items`"
+ @click="updateAttribute(attrItem)"
+ >
+ {{ attrItem.title }}
+ <template v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</template>
+ </gl-dropdown-item>
+ </slot>
+ </template>
+ <template #footer>
+ <slot name="footer"></slot>
+ </template>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index c33b1468ca4..a685929cdea 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -1,17 +1,5 @@
<script>
-import {
- GlLink,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlDropdownDivider,
- GlLoadingIcon,
- GlIcon,
- GlTooltipDirective,
- GlPopover,
- GlButton,
-} from '@gitlab/ui';
+import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -22,19 +10,15 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
dropdowni18nText,
- Tracking,
- IssuableAttributeState,
- IssuableAttributeType,
LocalizedIssuableAttributeType,
IssuableAttributeTypeKeyMap,
issuableAttributesQueries,
- noAttributeId,
- defaultEpicSort,
- epicIidPattern,
+ IssuableAttributeType,
+ Tracking,
} from 'ee_else_ce/sidebar/constants';
+import SidebarDropdown from './sidebar_dropdown.vue';
export default {
- noAttributeId,
i18n: {
expired: __('(expired)'),
none: __('None'),
@@ -43,17 +27,12 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
- SidebarEditableItem,
GlLink,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlDropdownDivider,
- GlSearchBoxByType,
GlIcon,
- GlLoadingIcon,
GlPopover,
GlButton,
+ SidebarDropdown,
+ SidebarEditableItem,
},
mixins: [glFeatureFlagMixin()],
inject: {
@@ -63,9 +42,6 @@ export default {
issuableAttributesQueries: {
default: issuableAttributesQueries,
},
- issuableAttributesState: {
- default: IssuableAttributeState,
- },
widgetTitleText: {
default: {
[IssuableAttributeType.Milestone]: __('Milestone'),
@@ -74,7 +50,6 @@ export default {
},
},
},
-
props: {
issuableAttribute: {
type: String,
@@ -134,67 +109,14 @@ export default {
});
},
},
- attributesList: {
- query() {
- const { list } = this.issuableAttributeQuery;
- const { query } = list[this.issuableType];
-
- return query;
- },
- skip() {
- if (this.isEpic && this.searchTerm.startsWith('&') && this.searchTerm.length < 2) {
- return true;
- }
-
- return !this.editing;
- },
- debounce: 250,
- variables() {
- if (!this.isEpic) {
- return {
- fullPath: this.attrWorkspacePath,
- title: this.searchTerm,
- state: this.issuableAttributesState[this.issuableAttribute],
- };
- }
-
- const variables = {
- fullPath: this.attrWorkspacePath,
- state: this.issuableAttributesState[this.issuableAttribute],
- sort: defaultEpicSort,
- };
-
- if (epicIidPattern.test(this.searchTerm)) {
- const matches = this.searchTerm.match(epicIidPattern);
- variables.iidStartsWith = matches.groups.iid;
- } else if (this.searchTerm !== '') {
- variables.in = 'TITLE';
- variables.title = this.searchTerm;
- }
-
- return variables;
- },
- update(data) {
- if (data?.workspace) {
- return data?.workspace?.attributes.nodes;
- }
- return [];
- },
- error(error) {
- createAlert({ message: this.i18n.listFetchError, captureError: true, error });
- },
- },
},
data() {
return {
- searchTerm: '',
- editing: false,
updating: false,
selectedTitle: null,
currentAttribute: null,
hasCurrentAttribute: false,
editConfirmation: false,
- attributesList: [],
tracking: {
event: Tracking.editEvent,
label: Tracking.rightSidebarLabel,
@@ -212,15 +134,9 @@ export default {
attributeUrl() {
return this.currentAttribute?.webUrl;
},
- dropdownText() {
- return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle;
- },
loading() {
return this.$apollo.queries.currentAttribute.loading;
},
- emptyPropsList() {
- return this.attributesList.length === 0;
- },
attributeTypeTitle() {
return this.widgetTitleText[this.issuableAttribute];
},
@@ -256,16 +172,12 @@ export default {
},
},
methods: {
- updateAttribute(attributeId) {
- if (this.currentAttribute === null && attributeId === null) return;
- if (attributeId === this.currentAttribute?.id) return;
+ updateAttribute({ id }) {
+ if (this.currentAttribute === null && id === null) return;
+ if (id === this.currentAttribute?.id) return;
this.updating = true;
- const selectedAttribute =
- Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId);
- this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.widgetTitleText.none;
-
const { current } = this.issuableAttributeQuery;
const { mutation } = current[this.issuableType];
@@ -277,8 +189,8 @@ export default {
attributeId:
this.issuableAttribute === IssuableAttributeType.Milestone &&
this.issuableType === IssuableType.Issue
- ? getIdFromGraphQLId(attributeId)
- : attributeId,
+ ? getIdFromGraphQLId(id)
+ : id,
iid: this.iid,
},
})
@@ -298,32 +210,16 @@ export default {
})
.finally(() => {
this.updating = false;
- this.searchTerm = '';
this.selectedTitle = null;
});
},
- isAttributeChecked(attributeId = undefined) {
- return (
- attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId)
- );
- },
isAttributeOverdue(attribute) {
return this.issuableAttribute === IssuableAttributeType.Milestone
? attribute?.expired
: false;
},
showDropdown() {
- this.$refs.newDropdown.show();
- },
- handleOpen() {
- this.editing = true;
- this.showDropdown();
- },
- handleClose() {
- this.editing = false;
- },
- setFocus() {
- this.$refs.search.focusInput();
+ this.$refs.dropdown.show();
},
handlePopoverClose() {
this.$refs.popover.$emit('close');
@@ -349,8 +245,7 @@ export default {
:tracking="tracking"
:should-show-confirmation-popover="shouldShowConfirmationPopover"
:loading="updating || loading"
- @open="handleOpen"
- @close="handleClose"
+ @open="showDropdown"
@edit-confirm="handleEditConfirmation"
>
<template #collapsed>
@@ -432,58 +327,24 @@ export default {
</gl-popover>
</template>
<template v-else #default>
- <gl-dropdown
- ref="newDropdown"
- lazy
- :header-text="i18n.assignAttribute"
- :text="dropdownText"
- :loading="loading"
- class="gl-w-full"
- toggle-class="gl-max-w-100"
- block
- @shown="setFocus"
+ <sidebar-dropdown
+ ref="dropdown"
+ :attr-workspace-path="attrWorkspacePath"
+ :current-attribute="currentAttribute"
+ :issuable-attribute="issuableAttribute"
+ :issuable-type="issuableType"
+ @change="updateAttribute"
>
- <gl-search-box-by-type ref="search" v-model="searchTerm" />
- <gl-dropdown-item
- :data-testid="`no-${formatIssuableAttribute.kebab}-item`"
- is-check-item
- :is-checked="isAttributeChecked($options.noAttributeId)"
- @click="updateAttribute($options.noAttributeId)"
- >
- {{ i18n.noAttribute }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-loading-icon
- v-if="$apollo.queries.attributesList.loading"
- size="sm"
- class="gl-py-4"
- data-testid="loading-icon-dropdown"
- />
- <template v-else>
- <gl-dropdown-text v-if="emptyPropsList">
- {{ i18n.noAttributesFound }}
- </gl-dropdown-text>
+ <template #list="{ attributesList, isAttributeChecked, updateAttribute: update }">
<slot
- v-else
name="list"
:attributes-list="attributesList"
:is-attribute-checked="isAttributeChecked"
- :update-attribute="updateAttribute"
+ :update-attribute="update"
>
- <gl-dropdown-item
- v-for="attrItem in attributesList"
- :key="attrItem.id"
- is-check-item
- :is-checked="isAttributeChecked(attrItem.id)"
- :data-testid="`${formatIssuableAttribute.kebab}-items`"
- @click="updateAttribute(attrItem.id)"
- >
- {{ attrItem.title }}
- <span v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</span>
- </gl-dropdown-item>
</slot>
</template>
- </gl-dropdown>
+ </sidebar-dropdown>
</template>
</sidebar-editable-item>
</template>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 6248bcb8e2d..67b9b540e91 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -53,6 +53,7 @@ import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/querie
import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql';
import updateEscalationStatusMutation from '~/sidebar/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';
@@ -241,10 +242,16 @@ export const issuableMilestoneQueries = {
export const milestonesQueries = {
[IssuableType.Issue]: {
- query: projectMilestonesQuery,
+ query: {
+ [WorkspaceType.group]: groupMilestonesQuery,
+ [WorkspaceType.project]: projectMilestonesQuery,
+ },
},
[IssuableType.MergeRequest]: {
- query: projectMilestonesQuery,
+ query: {
+ [WorkspaceType.group]: groupMilestonesQuery,
+ [WorkspaceType.project]: projectMilestonesQuery,
+ },
},
};
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index cc5de5e4083..afce59d304f 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -5,7 +5,7 @@ import TimeTracker from './components/time_tracking/time_tracker.vue';
export default class SidebarMilestone {
constructor() {
- const el = document.getElementById('issuable-time-tracker');
+ const el = document.querySelector('.js-sidebar-time-tracking-root');
if (!el) return;
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 9b5bad710dd..b37486283ca 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -18,6 +18,7 @@ import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assi
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
+import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
@@ -33,6 +34,7 @@ import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
import SidebarEscalationStatus from './components/incidents/sidebar_escalation_status.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
+import SidebarReviewersInputs from './components/reviewers/sidebar_reviewers_inputs.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
@@ -47,27 +49,24 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return JSON.parse(sidebarOptEl.innerHTML);
}
-function mountSidebarToDoWidget() {
- const el = document.querySelector('.js-issuable-todo');
+function mountSidebarTodoWidget() {
+ const el = document.querySelector('.js-sidebar-todo-widget-root');
if (!el) {
- return false;
+ return null;
}
const { projectPath, iid, id } = el.dataset;
return new Vue({
el,
- name: 'SidebarTodoRoot',
+ name: 'SidebarTodoWidgetRoot',
apolloProvider,
- components: {
- SidebarTodoWidget,
- },
provide: {
isClassicSidebar: true,
},
render: (createElement) =>
- createElement('sidebar-todo-widget', {
+ createElement(SidebarTodoWidget, {
props: {
fullPath: projectPath,
issuableId:
@@ -97,23 +96,22 @@ function getSidebarAssigneeAvailabilityData() {
);
}
-function mountAssigneesComponentDeprecated(mediator) {
- const el = document.getElementById('js-vue-sidebar-assignees');
+function mountSidebarAssigneesDeprecated(mediator) {
+ const el = document.querySelector('.js-sidebar-assignees-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { id, iid, fullPath } = getSidebarOptions();
const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData();
- // eslint-disable-next-line no-new
- new Vue({
+
+ return new Vue({
el,
name: 'SidebarAssigneesRoot',
apolloProvider,
- components: {
- SidebarAssignees,
- },
render: (createElement) =>
- createElement('sidebar-assignees', {
+ createElement(SidebarAssignees, {
props: {
mediator,
issuableIid: String(iid),
@@ -131,10 +129,12 @@ function mountAssigneesComponentDeprecated(mediator) {
});
}
-function mountAssigneesComponent() {
- const el = document.getElementById('js-vue-sidebar-assignees');
+function mountSidebarAssigneesWidget() {
+ const el = document.querySelector('.js-sidebar-assignees-root');
- if (!el) return;
+ if (!el) {
+ return;
+ }
const { id, iid, fullPath, editable } = getSidebarOptions();
const isIssuablePage = isInIssuePage() || isInIncidentPage() || isInDesignPage();
@@ -144,9 +144,6 @@ function mountAssigneesComponent() {
el,
name: 'SidebarAssigneesRoot',
apolloProvider,
- components: {
- SidebarAssigneesWidget,
- },
provide: {
canUpdate: editable,
directlyInviteMembers: Object.prototype.hasOwnProperty.call(
@@ -155,7 +152,7 @@ function mountAssigneesComponent() {
),
},
render: (createElement) =>
- createElement('sidebar-assignees-widget', {
+ createElement(SidebarAssigneesWidget, {
props: {
iid: String(iid),
fullPath,
@@ -183,10 +180,12 @@ function mountAssigneesComponent() {
}
}
-function mountReviewersComponent(mediator) {
- const el = document.getElementById('js-vue-sidebar-reviewers');
+function mountSidebarReviewers(mediator) {
+ const el = document.querySelector('.js-sidebar-reviewers-root');
- if (!el) return;
+ if (!el) {
+ return;
+ }
const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new
@@ -194,11 +193,8 @@ function mountReviewersComponent(mediator) {
el,
name: 'SidebarReviewersRoot',
apolloProvider,
- components: {
- SidebarReviewers,
- },
render: (createElement) =>
- createElement('sidebar-reviewers', {
+ createElement(SidebarReviewers, {
props: {
mediator,
issuableIid: String(iid),
@@ -210,6 +206,18 @@ function mountReviewersComponent(mediator) {
}),
});
+ const reviewersInputEl = document.querySelector('.js-reviewers-inputs');
+
+ if (reviewersInputEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: reviewersInputEl,
+ render(createElement) {
+ return createElement(SidebarReviewersInputs);
+ },
+ });
+ }
+
const reviewerDropdown = document.querySelector('.js-sidebar-reviewer-dropdown');
if (reviewerDropdown) {
@@ -217,22 +225,21 @@ function mountReviewersComponent(mediator) {
}
}
-function mountCrmContactsComponent() {
- const el = document.getElementById('js-issue-crm-contacts');
+function mountSidebarCrmContacts() {
+ const el = document.querySelector('.js-sidebar-crm-contacts-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { issueId, groupIssuesPath } = el.dataset;
- // eslint-disable-next-line no-new
- new Vue({
+
+ return new Vue({
el,
name: 'SidebarCrmContactsRoot',
apolloProvider,
- components: {
- CrmContacts,
- },
render: (createElement) =>
- createElement('crm-contacts', {
+ createElement(CrmContacts, {
props: {
issueId,
groupIssuesPath,
@@ -241,28 +248,25 @@ function mountCrmContactsComponent() {
});
}
-function mountMilestoneSelect() {
- const el = document.querySelector('.js-milestone-select');
+function mountSidebarMilestoneWidget() {
+ const el = document.querySelector('.js-sidebar-milestone-widget-root');
if (!el) {
- return false;
+ return null;
}
const { canEdit, projectPath, issueIid } = el.dataset;
return new Vue({
el,
- name: 'SidebarMilestoneRoot',
+ name: 'SidebarMilestoneWidgetRoot',
apolloProvider,
- components: {
- SidebarDropdownWidget,
- },
provide: {
canUpdate: parseBoolean(canEdit),
isClassicSidebar: true,
},
render: (createElement) =>
- createElement('sidebar-dropdown-widget', {
+ createElement(SidebarDropdownWidget, {
props: {
attrWorkspacePath: projectPath,
workspacePath: projectPath,
@@ -276,21 +280,57 @@ function mountMilestoneSelect() {
});
}
-export function mountSidebarLabels() {
- const el = document.querySelector('.js-sidebar-labels');
+export function mountMilestoneDropdown() {
+ const el = document.querySelector('.js-milestone-dropdown-root');
if (!el) {
- return false;
+ return null;
}
+ Vue.use(VueApollo);
+
+ const {
+ canAdminMilestone,
+ fullPath,
+ inputName,
+ milestoneId,
+ milestoneTitle,
+ projectMilestonesPath,
+ workspaceType,
+ } = el.dataset;
+
return new Vue({
el,
- name: 'SidebarLabelsRoot',
+ name: 'MilestoneDropdownRoot',
apolloProvider,
-
- components: {
- LabelsSelectWidget,
+ render(createElement) {
+ return createElement(MilestoneDropdown, {
+ props: {
+ attrWorkspacePath: fullPath,
+ canAdminMilestone,
+ inputName,
+ issuableType: isInIssuePage() ? IssuableType.Issue : IssuableType.MergeRequest,
+ milestoneId,
+ milestoneTitle,
+ projectMilestonesPath,
+ workspaceType,
+ },
+ });
},
+ });
+}
+
+export function mountSidebarLabelsWidget() {
+ const el = document.querySelector('.js-sidebar-labels-widget-root');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'SidebarLabelsWidgetRoot',
+ apolloProvider,
provide: {
...el.dataset,
canUpdate: parseBoolean(el.dataset.canEdit),
@@ -300,7 +340,7 @@ export function mountSidebarLabels() {
isClassicSidebar: true,
},
render: (createElement) =>
- createElement('labels-select-widget', {
+ createElement(LabelsSelectWidget, {
props: {
iid: String(el.dataset.iid),
fullPath: el.dataset.projectPath,
@@ -327,31 +367,27 @@ export function mountSidebarLabels() {
});
}
-function mountConfidentialComponent() {
- const el = document.getElementById('js-confidential-entry-point');
+function mountSidebarConfidentialityWidget() {
+ const el = document.querySelector('.js-sidebar-confidential-widget-root');
+
if (!el) {
- return;
+ return null;
}
const { fullPath, iid } = getSidebarOptions();
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarConfidentialRoot',
+ name: 'SidebarConfidentialityWidgetRoot',
apolloProvider,
- components: {
- SidebarConfidentialityWidget,
- },
provide: {
canUpdate: initialData.is_editable,
isClassicSidebar: true,
},
-
render: (createElement) =>
- createElement('sidebar-confidentiality-widget', {
+ createElement(SidebarConfidentialityWidget, {
props: {
iid: String(iid),
fullPath,
@@ -364,28 +400,24 @@ function mountConfidentialComponent() {
});
}
-function mountDueDateComponent() {
- const el = document.getElementById('js-due-date-entry-point');
+function mountSidebarDueDateWidget() {
+ const el = document.querySelector('.js-sidebar-due-date-widget-root');
+
if (!el) {
- return;
+ return null;
}
const { fullPath, iid, editable } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarDueDateRoot',
+ name: 'SidebarDueDateWidgetRoot',
apolloProvider,
- components: {
- SidebarDueDateWidget,
- },
provide: {
canUpdate: editable,
},
-
render: (createElement) =>
- createElement('sidebar-due-date-widget', {
+ createElement(SidebarDueDateWidget, {
props: {
iid: String(iid),
fullPath,
@@ -395,29 +427,25 @@ function mountDueDateComponent() {
});
}
-function mountReferenceComponent() {
- const el = document.getElementById('js-reference-entry-point');
+function mountSidebarReferenceWidget() {
+ const el = document.querySelector('.js-sidebar-reference-widget-root');
+
if (!el) {
- return;
+ return null;
}
const { fullPath, iid } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarReferenceRoot',
+ name: 'SidebarReferenceWidgetRoot',
apolloProvider,
- components: {
- SidebarReferenceWidget,
- },
provide: {
iid: String(iid),
fullPath,
},
-
render: (createElement) =>
- createElement('sidebar-reference-widget', {
+ createElement(SidebarReferenceWidget, {
props: {
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
@@ -428,17 +456,16 @@ function mountReferenceComponent() {
});
}
-function mountLockComponent(store) {
- const el = document.getElementById('js-lock-entry-point');
+function mountIssuableLockForm(store) {
+ const el = document.querySelector('.js-sidebar-lock-root');
if (!el || !store) {
- return;
+ return null;
}
const { fullPath, editable } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
name: 'SidebarLockRoot',
store,
@@ -454,23 +481,21 @@ function mountLockComponent(store) {
});
}
-function mountParticipantsComponent() {
- const el = document.querySelector('.js-sidebar-participants-entry-point');
+function mountSidebarParticipantsWidget() {
+ const el = document.querySelector('.js-sidebar-participants-widget-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { fullPath, iid } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarParticipantsRoot',
+ name: 'SidebarParticipantsWidgetRoot',
apolloProvider,
- components: {
- SidebarParticipantsWidget,
- },
render: (createElement) =>
- createElement('sidebar-participants-widget', {
+ createElement(SidebarParticipantsWidget, {
props: {
iid: String(iid),
fullPath,
@@ -483,26 +508,24 @@ function mountParticipantsComponent() {
});
}
-function mountSubscriptionsComponent() {
- const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+function mountSidebarSubscriptionsWidget() {
+ const el = document.querySelector('.js-sidebar-subscriptions-widget-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { fullPath, iid, editable } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarSubscriptionsRoot',
+ name: 'SidebarSubscriptionsWidgetRoot',
apolloProvider,
- components: {
- SidebarSubscriptionsWidget,
- },
provide: {
canUpdate: editable,
},
render: (createElement) =>
- createElement('sidebar-subscriptions-widget', {
+ createElement(SidebarSubscriptionsWidget, {
props: {
iid: String(iid),
fullPath,
@@ -515,14 +538,15 @@ function mountSubscriptionsComponent() {
});
}
-function mountTimeTrackingComponent() {
- const el = document.getElementById('issuable-time-tracker');
+function mountSidebarTimeTracking() {
+ const el = document.querySelector('.js-sidebar-time-tracking-root');
const { id, iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
- if (!el) return;
+ if (!el) {
+ return null;
+ }
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
name: 'SidebarTimeTrackingRoot',
apolloProvider,
@@ -539,27 +563,24 @@ function mountTimeTrackingComponent() {
});
}
-function mountSeverityComponent() {
- const severityContainerEl = document.querySelector('#js-severity');
+function mountSidebarSeverity() {
+ const el = document.querySelector('.js-sidebar-severity-root');
- if (!severityContainerEl) {
- return false;
+ if (!el) {
+ return null;
}
const { fullPath, iid, severity, editable } = getSidebarOptions();
return new Vue({
- el: severityContainerEl,
+ el,
name: 'SidebarSeverityRoot',
apolloProvider,
- components: {
- SidebarSeverity,
- },
provide: {
canUpdate: editable,
},
render: (createElement) =>
- createElement('sidebar-severity', {
+ createElement(SidebarSeverity, {
props: {
projectPath: fullPath,
iid: String(iid),
@@ -569,27 +590,25 @@ function mountSeverityComponent() {
});
}
-function mountEscalationStatusComponent() {
- const statusContainerEl = document.querySelector('#js-escalation-status');
+function mountSidebarEscalationStatus() {
+ const el = document.querySelector('.js-sidebar-escalation-status-root');
- if (!statusContainerEl) {
- return false;
+ if (!el) {
+ return null;
}
const { issuableType } = getSidebarOptions();
- const { canUpdate, issueIid, projectPath } = statusContainerEl.dataset;
+ const { canUpdate, issueIid, projectPath } = el.dataset;
return new Vue({
- el: statusContainerEl,
+ el,
+ name: 'SidebarEscalationStatusRoot',
apolloProvider,
- components: {
- SidebarEscalationStatus,
- },
provide: {
canUpdate: parseBoolean(canUpdate),
},
render: (createElement) =>
- createElement('sidebar-escalation-status', {
+ createElement(SidebarEscalationStatus, {
props: {
iid: issueIid,
issuableType,
@@ -599,15 +618,16 @@ function mountEscalationStatusComponent() {
});
}
-function mountCopyEmailComponent() {
- const el = document.getElementById('issuable-copy-email');
+function mountCopyEmailToClipboard() {
+ const el = document.querySelector('.js-sidebar-copy-email-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { createNoteEmail } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
name: 'SidebarCopyEmailRoot',
render: (createElement) =>
@@ -621,36 +641,32 @@ const isAssigneesWidgetShown =
export function mountSidebar(mediator, store) {
initInviteMembersModal();
initInviteMembersTrigger();
-
- mountSidebarToDoWidget();
+ mountSidebarTodoWidget();
if (isAssigneesWidgetShown) {
- mountAssigneesComponent();
+ mountSidebarAssigneesWidget();
} else {
- mountAssigneesComponentDeprecated(mediator);
+ mountSidebarAssigneesDeprecated(mediator);
}
- mountReviewersComponent(mediator);
- mountCrmContactsComponent();
- mountSidebarLabels();
- mountMilestoneSelect();
- mountConfidentialComponent(mediator);
- mountDueDateComponent(mediator);
- mountReferenceComponent(mediator);
- mountLockComponent(store);
- mountParticipantsComponent();
- mountSubscriptionsComponent();
- mountCopyEmailComponent();
+ mountSidebarReviewers(mediator);
+ mountSidebarCrmContacts();
+ mountSidebarLabelsWidget();
+ mountSidebarMilestoneWidget();
+ mountSidebarConfidentialityWidget();
+ mountSidebarDueDateWidget();
+ mountSidebarReferenceWidget();
+ mountIssuableLockForm(store);
+ mountSidebarParticipantsWidget();
+ mountSidebarSubscriptionsWidget();
+ mountCopyEmailToClipboard();
+ mountSidebarTimeTracking();
+ mountSidebarSeverity();
+ mountSidebarEscalationStatus();
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
-
- mountTimeTrackingComponent();
-
- mountSeverityComponent();
-
- mountEscalationStatusComponent();
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index e2581a8f30e..baf906bb96c 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -138,6 +138,10 @@ export default class SidebarStore {
this.assignees = data;
}
+ setReviewersFromRealtime(data) {
+ this.reviewers = data;
+ }
+
setAutocompleteProjects(projects) {
this.autocompleteProjects = projects;
}
diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue
index 4b91872d80d..fe99f3e1fdd 100644
--- a/app/assets/javascripts/token_access/components/token_access.vue
+++ b/app/assets/javascripts/token_access/components/token_access.vue
@@ -117,7 +117,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createAlert({ message: error });
+ createAlert({ message: error.message });
}
},
async addProject() {
@@ -140,7 +140,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createAlert({ message: error });
+ createAlert({ message: error.message });
} finally {
this.clearTargetProjectPath();
this.getProjects();
@@ -166,7 +166,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createAlert({ message: error });
+ createAlert({ message: error.message });
} finally {
this.getProjects();
}
diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue
index 82ef3371d91..ce33478cbee 100644
--- a/app/assets/javascripts/token_access/components/token_projects_table.vue
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -10,14 +10,21 @@ export default {
{
key: 'project',
label: __('Projects that can be accessed'),
- tdClass: 'gl-p-5!',
- columnClass: 'gl-w-85p',
+ thClass: 'gl-border-t-none!',
+ columnClass: 'gl-w-40p',
+ },
+ {
+ key: 'namespace',
+ label: __('Namespace'),
+ thClass: 'gl-border-t-none!',
+ columnClass: 'gl-w-40p',
},
{
key: 'actions',
label: '',
- tdClass: 'gl-p-5! gl-text-right',
- columnClass: 'gl-w-15p',
+ tdClass: 'gl-text-right',
+ thClass: 'gl-border-t-none!',
+ columnClass: 'gl-w-10p',
},
],
components: {
@@ -57,7 +64,11 @@ export default {
</template>
<template #cell(project)="{ item }">
- {{ item.name }}
+ <span data-testid="token-access-project-name">{{ item.name }}</span>
+ </template>
+
+ <template #cell(namespace)="{ item }">
+ <span data-testid="token-access-project-namespace">{{ item.namespace.fullPath }}</span>
</template>
<template #cell(actions)="{ item }">
diff --git a/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
index 664991bc110..a243095f1b4 100644
--- a/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
+++ b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
@@ -6,6 +6,10 @@ query getProjectsWithCIJobTokenScope($fullPath: ID!) {
nodes {
id
name
+ namespace {
+ id
+ fullPath
+ }
fullPath
}
}
diff --git a/app/assets/javascripts/tracking/tracker.js b/app/assets/javascripts/tracking/tracker.js
index 9ad86e76b6e..85f4979e752 100644
--- a/app/assets/javascripts/tracking/tracker.js
+++ b/app/assets/javascripts/tracking/tracker.js
@@ -39,8 +39,8 @@ export const Tracker = {
},
/**
- * Dispatches a structured event per our taxonomy:
- * https://docs.gitlab.com/ee/development/snowplow/index.html#structured-event-taxonomy.
+ * Dispatches a structured event:
+ * https://docs.gitlab.com/ee/development/snowplow/index.html#event-schema.
*
* If the library is not initialized and events are trying to be
* dispatched (data-attributes, load-events), they will be added
@@ -49,7 +49,7 @@ export const Tracker = {
* If there is an error when using the library, it will return ´false´
* and ´true´ otherwise.
*
- * @param {...any} eventData defined event taxonomy
+ * @param {...any} eventData defined event schema
* @returns {Boolean}
*/
event(...eventData) {
diff --git a/app/assets/javascripts/users_select/constants.js b/app/assets/javascripts/users_select/constants.js
index 64df1e1748c..c100c1f4ca5 100644
--- a/app/assets/javascripts/users_select/constants.js
+++ b/app/assets/javascripts/users_select/constants.js
@@ -1,11 +1,3 @@
-export const AJAX_USERS_SELECT_OPTIONS_MAP = {
- projectId: 'projectId',
- groupId: 'groupId',
- showCurrentUser: 'currentUser',
- authorId: 'authorId',
- skipUsers: 'skipUsers',
-};
-
export const AJAX_USERS_SELECT_PARAMS_MAP = {
project_id: 'projectId',
group_id: 'groupId',
@@ -15,4 +7,7 @@ export const AJAX_USERS_SELECT_PARAMS_MAP = {
current_user: 'showCurrentUser',
author_id: 'authorId',
skip_users: 'skipUsers',
+ states: 'states',
};
+
+export const ACTIVE_AND_BLOCKED_USER_STATES = ['active', 'blocked'];
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index bd425bdc2a8..7c1204c511c 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -1,21 +1,17 @@
-/* eslint-disable func-names, prefer-rest-params, consistent-return, no-shadow, no-self-compare, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
+/* eslint-disable func-names, consistent-return, no-shadow, no-self-compare, no-unused-expressions, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
import $ from 'jquery';
import { escape, template, uniqBy } from 'lodash';
-import {
- AJAX_USERS_SELECT_OPTIONS_MAP,
- AJAX_USERS_SELECT_PARAMS_MAP,
-} from 'ee_else_ce/users_select/constants';
+import { AJAX_USERS_SELECT_PARAMS_MAP } from 'ee_else_ce/users_select/constants';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { isUserBusy } from '~/set_status_modal/utils';
import { fixTitle, dispose } from '~/tooltips';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean, spriteIcon } from '~/lib/utils/common_utils';
-import { loadCSSFile } from '~/lib/utils/css_utils';
import { s__, __, sprintf } from '~/locale';
-import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from './utils';
+import { getAjaxUsersSelectParams } from './utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@@ -24,9 +20,7 @@ function UsersSelect(currentUser, els, options = {}) {
const elsClassName = els?.toString().match('.(.+$)')[1];
const $els = $(els || '.js-user-search');
this.users = this.users.bind(this);
- this.user = this.user.bind(this);
this.usersPath = '/-/autocomplete/users.json';
- this.userPath = '/-/autocomplete/users/:id.json';
if (currentUser != null) {
if (typeof currentUser === 'object') {
this.currentUser = currentUser;
@@ -35,28 +29,29 @@ function UsersSelect(currentUser, els, options = {}) {
}
}
- const { handleClick } = options;
- const userSelect = this;
+ const { handleClick, states } = options;
$els.each((i, dropdown) => {
const userSelect = this;
- const options = {};
const $dropdown = $(dropdown);
- options.projectId = $dropdown.data('projectId');
- options.groupId = $dropdown.data('groupId');
- options.showCurrentUser = $dropdown.data('currentUser');
- options.todoFilter = $dropdown.data('todoFilter');
- options.todoStateFilter = $dropdown.data('todoStateFilter');
- options.iid = $dropdown.data('iid');
- options.issuableType = $dropdown.data('issuableType');
- options.targetBranch = $dropdown.data('targetBranch');
- options.showSuggested = $dropdown.data('showSuggested');
+ 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');
- options.authorId = $dropdown.data('authorId');
const defaultLabel = $dropdown.data('defaultLabel');
const issueURL = $dropdown.data('issueUpdate');
const $selectbox = $dropdown.closest('.selectbox');
@@ -72,6 +67,22 @@ function UsersSelect(currentUser, els, options = {}) {
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')}"
+ href="${suggestedReviewersHelpPath}"
+ rel="noopener"
+ target="_blank"
+ aria-label="${s__('SuggestedReviewers|Suggested reviewers help link')}"
+ class="gl-hover-bg-transparent! gl-p-0! has-tooltip">
+ ${spriteIcon('question-o', 'gl-ml-3 gl-icon s16')}
+ </a>
+ </div>`,
+ );
+
if (selectedId === undefined) {
selectedId = selectedIdDefault;
}
@@ -388,7 +399,12 @@ function UsersSelect(currentUser, els, options = {}) {
if (!suggestedUsers.length) return [];
const items = [
- { type: 'header', content: $dropdown.data('suggestedReviewersHeader') },
+ {
+ type: 'header',
+ content: suggestedReviewersHeaderTemplate({
+ header: $dropdown.data('suggestedReviewersHeader'),
+ }),
+ },
...suggestedUsers,
{ type: 'header', content: $dropdown.data('allMembersHeader') },
];
@@ -431,6 +447,10 @@ function UsersSelect(currentUser, els, options = {}) {
hidden() {
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass(elsClassName)) {
+ if (window.gon?.features?.realtimeReviewers) {
+ $dropdown.data('deprecatedJQueryDropdown').clearMenu();
+ $dropdown.closest('.selectbox').children('input[type="hidden"]').remove();
+ }
emitSidebarEvent('sidebar.saveReviewers');
} else {
emitSidebarEvent('sidebar.saveAssignees');
@@ -615,156 +635,8 @@ function UsersSelect(currentUser, els, options = {}) {
},
});
});
-
- if ($('.ajax-users-select').length) {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- $('.ajax-users-select').each((i, select) => {
- const options = getAjaxUsersSelectOptions($(select), AJAX_USERS_SELECT_OPTIONS_MAP);
- options.skipLdap = $(select).hasClass('skip_ldap');
- const showNullUser = $(select).data('nullUser');
- const showAnyUser = $(select).data('anyUser');
- const showEmailUser = $(select).data('emailUser');
- const firstUser = $(select).data('firstUser');
- return $(select).select2({
- placeholder: __('Search for a user'),
- multiple: $(select).hasClass('multiselect'),
- minimumInputLength: 0,
- query(query) {
- return userSelect.users(query.term, options, (users) => {
- let name;
- const data = {
- results: users,
- };
- if (query.term.length === 0) {
- if (firstUser) {
- // Move current user to the front of the list
- const ref = data.results;
-
- for (let index = 0, len = ref.length; index < len; index += 1) {
- const obj = ref[index];
- if (obj.username === firstUser) {
- data.results.splice(index, 1);
- data.results.unshift(obj);
- break;
- }
- }
- }
- if (showNullUser) {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- id: 0,
- };
- data.results.unshift(nullUser);
- }
- if (showAnyUser) {
- name = showAnyUser;
- if (name === true) {
- name = s__('UsersSelect|Any User');
- }
- const anyUser = {
- name,
- id: null,
- };
- data.results.unshift(anyUser);
- }
- }
- if (
- showEmailUser &&
- data.results.length === 0 &&
- query.term.match(/^[^@]+@[^@]+$/)
- ) {
- const trimmed = query.term.trim();
- const emailUser = {
- name: sprintf(__('Invite "%{trimmed}" by email'), { trimmed }),
- username: trimmed,
- id: trimmed,
- invite: true,
- };
- data.results.unshift(emailUser);
- }
- return query.callback(data);
- });
- },
- initSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.initSelection.apply(userSelect, args);
- },
- formatResult() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatResult.apply(userSelect, args);
- },
- formatSelection() {
- const args = 1 <= arguments.length ? [].slice.call(arguments, 0) : [];
- return userSelect.formatSelection.apply(userSelect, args);
- },
- dropdownCssClass: 'ajax-users-dropdown',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
- }
}
-UsersSelect.prototype.initSelection = function (element, callback) {
- const id = $(element).val();
- if (id === '0') {
- const nullUser = {
- name: s__('UsersSelect|Unassigned'),
- };
- return callback(nullUser);
- } else if (id !== '') {
- return this.user(id, callback);
- }
-};
-
-UsersSelect.prototype.formatResult = function (user) {
- let avatar = gon.default_avatar_url;
- if (user.avatar_url) {
- avatar = user.avatar_url;
- }
- return `
- <div class='user-result'>
- <div class='user-image'>
- <img class='avatar avatar-inline s32' src='${avatar}'>
- </div>
- <div class='user-info'>
- <div class='user-name dropdown-menu-user-full-name'>
- ${escape(user.name)}
- </div>
- <div class='user-username dropdown-menu-user-username text-secondary'>
- ${!user.invite ? `@${escape(user.username)}` : ''}
- </div>
- </div>
- </div>
- `;
-};
-
-UsersSelect.prototype.formatSelection = function (user) {
- return escape(user.name);
-};
-
-UsersSelect.prototype.user = function (user_id, callback) {
- if (!/^\d+$/.test(user_id)) {
- return false;
- }
-
- let url = this.buildUrl(this.userPath);
- url = url.replace(':id', user_id);
- return axios.get(url).then(({ data }) => {
- callback(data);
- });
-};
-
// Return users list. Filtered by query
// Only active users retrieved
UsersSelect.prototype.users = function (query, options, callback) {
diff --git a/app/assets/javascripts/users_select/utils.js b/app/assets/javascripts/users_select/utils.js
index b46fd15fb77..8184e646124 100644
--- a/app/assets/javascripts/users_select/utils.js
+++ b/app/assets/javascripts/users_select/utils.js
@@ -1,18 +1,4 @@
/**
- * Get options from data attributes on passed `$select`.
- * @param {jQuery} $select
- * @param {Object} optionsMap e.g. { optionKeyName: 'dataAttributeName' }
- */
-export const getAjaxUsersSelectOptions = ($select, optionsMap) => {
- return Object.keys(optionsMap).reduce((accumulator, optionKey) => {
- const dataKey = optionsMap[optionKey];
- accumulator[optionKey] = $select.data(dataKey);
-
- return accumulator;
- }, {});
-};
-
-/**
* Get query parameters used for users request from passed `options` parameter
* @param {Object} options e.g. { currentUserId: 1, fooBar: 'baz' }
* @param {Object} paramsMap e.g. { user_id: 'currentUserId', foo_bar: 'fooBar' }
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
index 30098f7619a..2132c2953d9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_info.vue
@@ -1,7 +1,6 @@
<script>
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { GlLink, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
import { __ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
MANUAL_DEPLOY,
@@ -18,7 +17,7 @@ export default {
components: {
GlLink,
MemoryUsage: () => import('./memory_usage.vue'),
- TooltipOnTruncate,
+ GlTruncate,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -74,16 +73,19 @@ export default {
<div class="js-deployment-info deployment-info">
<template v-if="hasDeploymentMeta">
<span>{{ deployedText }}</span>
- <tooltip-on-truncate :title="deployment.name" truncate-target="child" class="label-truncate">
- <gl-link
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-meta gl-font-sm gl-pb-1"
- >
- {{ deployment.name }}
- </gl-link>
- </tooltip-on-truncate>
+ <gl-link
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta gl-font-sm gl-pb-1"
+ >
+ <gl-truncate
+ class="js-deploy-env-name"
+ :text="deployment.name"
+ position="middle"
+ with-tooltip
+ />
+ </gl-link>
</template>
<span
v-if="hasDeploymentTime"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
index 655acf28253..501f5f1523f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_list.vue
@@ -67,7 +67,6 @@ export default {
v-for="deployment in deployments"
:key="deployment.id"
:class="deploymentClass"
- class="gl-bg-gray-50"
:deployment="deployment"
:show-metrics="hasDeploymentMetrics"
/>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 5efa0e2879e..39a6086e0d5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -72,7 +72,7 @@ export default {
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
- css-class="deploy-link js-deploy-url inline gl-ml-3"
+ css-class="deploy-link js-deploy-url inline"
/>
<modal-copy-button
v-else
@@ -116,7 +116,7 @@ export default {
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
- css-class="deploy-link js-deploy-url inline gl-ml-3"
+ css-class="deploy-link js-deploy-url inline"
/>
<modal-copy-button
v-else
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
index a58d524b9ed..cc7b9d0ea72 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js
@@ -11,8 +11,6 @@ export default {
render(h) {
const { extensions } = registeredExtensions;
- if (extensions.length === 0) return null;
-
return h(
'section',
{
@@ -22,26 +20,28 @@ export default {
},
},
[
- h(
- 'ul',
- {
- class: 'gl-p-0 gl-m-0 gl-list-style-none',
- },
- [
- ...extensions.map((extension, index) =>
- h('li', { attrs: { class: index > 0 && 'mr-widget-border-top' } }, [
- h(
- { ...extension },
- {
- props: {
- mr: this.mr,
+ h('div', { attrs: { class: 'mr-widget-section' } }, [
+ h(
+ 'ul',
+ {
+ class: 'gl-p-0 gl-m-0 gl-list-style-none',
+ },
+ [
+ ...extensions.map((extension, index) =>
+ h('li', { attrs: { class: index > 0 && 'mr-widget-border-top' } }, [
+ h(
+ { ...extension },
+ {
+ props: {
+ mr: this.mr,
+ },
},
- },
- ),
- ]),
- ),
- ],
- ),
+ ),
+ ]),
+ ),
+ ],
+ ),
+ ]),
],
);
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index f8d2732b385..6475def461a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -46,8 +46,8 @@ export default {
};
</script>
<template>
- <div>
- <div class="mr-widget-extension d-flex align-items-center pl-3">
+ <div class="mr-widget-extension">
+ <div class="d-flex align-items-center pl-3">
<div v-if="hasError" class="ci-widget media">
<div class="media-body">
<span class="gl-font-sm mr-widget-margin-left gl-line-height-24 js-error-state">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
index 5967ca026e5..4445448fcd7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_container.vue
@@ -1,6 +1,8 @@
<template>
- <div class="mr-widget-heading">
- <div class="mr-widget-content"><slot name="default"></slot></div>
- <slot name="footer"></slot>
+ <div class="mr-section-container mr-widget-workflow">
+ <div class="mr-widget-section">
+ <div class="mr-widget-content"><slot name="default"></slot></div>
+ <slot name="footer"></slot>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index fe69e96bd87..97c6de37054 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
@@ -200,7 +200,7 @@ export default {
data-testid="pipeline-info-container"
data-qa-selector="merge_request_pipeline_info_content"
>
- {{ pipeline.details.name }}
+ {{ pipeline.details.event_type_name || pipeline.details.name }}
<gl-link
:href="pipeline.path"
class="pipeline-id gl-font-weight-normal pipeline-number"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index a05e8747a43..010c172c710 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -80,13 +80,7 @@ export default {
);
},
preferredAutoMergeStrategy() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return MergeRequestStore.getPreferredAutoMergeStrategy(
- this.mr.availableAutoMergeStrategies,
- );
- }
-
- return this.mr.preferredAutoMergeStrategy;
+ return MergeRequestStore.getPreferredAutoMergeStrategy(this.mr.availableAutoMergeStrategies);
},
ciStatus() {
return this.isPostMerge ? this.mr?.mergePipeline?.details?.status?.text : this.mr.ciStatus;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue
new file mode 100644
index 00000000000..2683956e603
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue
@@ -0,0 +1,18 @@
+<script>
+export default {
+ data() {
+ return {
+ hasChildren: false,
+ };
+ },
+ updated() {
+ this.hasChildren = this.$scopedSlots.default?.()?.some((c) => c.tag);
+ },
+};
+</script>
+
+<template>
+ <div v-show="hasChildren" class="mr-section-container mr-widget-workflow">
+ <slot></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index 932659f3c89..66e33a08a12 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -45,12 +45,19 @@ export default {
if (this.status === 'closed') return 'gl-bg-red-50';
return null;
},
+ hasActionsSlot() {
+ return this.$scopedSlots.actions?.()?.length;
+ },
},
};
</script>
<template>
- <div class="mr-widget-body media" :class="wrapperClasses" v-on="$listeners">
+ <div
+ class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
+ :class="wrapperClasses"
+ v-on="$listeners"
+ >
<div v-if="isLoading" class="gl-w-full mr-conflict-loader">
<slot name="loading">
<div class="gl-display-flex">
@@ -67,13 +74,19 @@ export default {
</slot>
<div class="gl-display-flex gl-w-full">
<div
- :class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }"
+ :class="{
+ 'gl-display-flex gl-align-items-center': actions.length,
+ 'gl-md-display-flex gl-align-items-center': !actions.length,
+ }"
class="media-body"
>
<slot></slot>
<div
- :class="{ 'gl-flex-direction-column-reverse': !actions.length }"
- class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
+ :class="{
+ 'state-container-action-buttons gl-flex-direction-column gl-flex-wrap gl-justify-content-end': !actions.length,
+ 'gl-md-pt-0 gl-pt-3': hasActionsSlot,
+ }"
+ class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3"
>
<slot name="actions">
<actions v-if="actions.length" :tertiary-buttons="actions" />
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 2b22033514f..38b99dae264 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
@@ -9,6 +9,7 @@ export default {
blockingMergeRequests: s__(
'mrWidget|Merge blocked: you can only merge after the above items are resolved.',
),
+ externalStatusChecksFailed: s__('mrWidget|Merge blocked: all status checks must pass.'),
},
components: {
StatusIcon,
@@ -25,6 +26,8 @@ export default {
return this.$options.i18n.approvalNeeded;
} else 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) {
+ return this.$options.i18n.externalStatusChecksFailed;
}
return null;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 92a7fa39cdc..38f7d3d2c96 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -3,8 +3,8 @@ import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import { createAlert } from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -16,9 +16,6 @@ export default {
apollo: {
state: {
query: autoMergeEnabledQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -31,7 +28,7 @@ export default {
GlSprintf,
StateContainer,
},
- mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [autoMergeMixin, mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
@@ -51,31 +48,21 @@ export default {
},
computed: {
loading() {
- return (
- this.glFeatures.mergeRequestWidgetGraphql &&
- this.$apollo.queries.state.loading &&
- Object.keys(this.state).length === 0
- );
- },
- mergeUser() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.mergeUser;
- }
-
- return this.mr.setToAutoMergeBy;
+ return this.$apollo.queries.state.loading && Object.keys(this.state).length === 0;
},
- targetBranch() {
- return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch;
- },
- shouldRemoveSourceBranch() {
- if (!this.glFeatures.mergeRequestWidgetGraphql) return this.mr.shouldRemoveSourceBranch;
-
+ stateRemoveSourceBranch() {
if (!this.state.shouldRemoveSourceBranch) return false;
return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
},
- autoMergeStrategy() {
- return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy;
+ canRemoveSourceBranch() {
+ const { currentUserId } = this.mr;
+ const mergeUserId = getIdFromGraphQLId(this.state.mergeUser?.id);
+ const canRemoveSourceBranch = this.state.userPermissions.removeSourceBranch;
+
+ return (
+ !this.stateRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId
+ );
},
actions() {
const actions = [];
@@ -104,12 +91,8 @@ export default {
this.service
.cancelAutomaticMerge()
.then((res) => res.data)
- .then((data) => {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- eventHub.$emit('MRWidgetUpdateRequested');
- } else {
- eventHub.$emit('UpdateWidgetData', data);
- }
+ .then(() => {
+ eventHub.$emit('MRWidgetUpdateRequested');
})
.catch(() => {
this.isCancellingAutoMerge = false;
@@ -121,7 +104,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
- auto_merge_strategy: this.autoMergeStrategy,
+ auto_merge_strategy: this.state.autoMergeStrategy,
should_remove_source_branch: true,
};
@@ -135,9 +118,7 @@ export default {
}
})
.then(() => {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- this.$apollo.queries.state.refetch();
- }
+ this.$apollo.queries.state.refetch();
})
.catch(() => {
this.isRemovingSourceBranch = false;
@@ -162,7 +143,7 @@ export default {
<h4 class="gl-mr-3" data-testid="statusText">
<gl-sprintf :message="statusText" data-testid="statusText">
<template #merge_author>
- <mr-widget-author :author="mergeUser" />
+ <mr-widget-author v-if="state.mergeUser" :author="state.mergeUser" />
</template>
</gl-sprintf>
</h4>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index 39c56cbb93d..448805cf8b9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -1,6 +1,5 @@
<script>
import { s__ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
@@ -11,13 +10,10 @@ export default {
components: {
StateContainer,
},
- mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [mergeRequestQueryVariablesMixin],
apollo: {
mergeError: {
query: autoMergeFailedQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -32,7 +28,7 @@ export default {
},
data() {
return {
- mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError,
+ mergeError: null,
isRefreshing: false,
};
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index d60d3cfc9ea..8e1b18c63a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
@@ -13,23 +12,17 @@ export default {
GlButton,
StateContainer,
},
- mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
query: userPermissionsQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project.mergeRequest.userPermissions,
},
- stateData: {
+ state: {
query: conflictsStateQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -47,40 +40,19 @@ export default {
data() {
return {
userPermissions: {},
- stateData: {},
+ state: {},
};
},
computed: {
isLoading() {
- return (
- this.glFeatures.mergeRequestWidgetGraphql &&
- this.$apollo.queries.userPermissions.loading &&
- this.$apollo.queries.stateData.loading
- );
- },
- canPushToSourceBranch() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.userPermissions.pushToSourceBranch;
- }
-
- return this.mr.canPushToSourceBranch;
- },
- canMerge() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.userPermissions.canMerge;
- }
-
- return this.mr.canMerge;
- },
- shouldBeRebased() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.stateData.shouldBeRebased;
- }
-
- return this.mr.shouldBeRebased;
+ return this.$apollo.queries.userPermissions.loading && this.$apollo.queries.state.loading;
},
showResolveButton() {
- return this.mr.conflictResolutionPath && this.canPushToSourceBranch;
+ return (
+ this.mr.conflictResolutionPath &&
+ this.userPermissions.pushToSourceBranch &&
+ !this.state.sourceBranchProtected
+ );
},
},
};
@@ -95,7 +67,7 @@ export default {
</gl-skeleton-loader>
</template>
<template v-if="!isLoading">
- <span v-if="shouldBeRebased" class="bold gl-ml-0! gl-text-body!">
+ <span v-if="state.shouldBeRebased" class="bold gl-ml-0! gl-text-body!">
{{
s__(`mrWidget|Merge blocked: fast-forward merge is not possible.
To merge this request, first rebase locally.`)
@@ -104,7 +76,7 @@ export default {
<template v-else>
<span class="bold gl-ml-0! gl-text-body! gl-flex-grow-1 gl-w-full gl-md-w-auto gl-mr-2">
{{ s__('mrWidget|Merge blocked: merge conflicts must be resolved.') }}
- <span v-if="!canMerge">
+ <span v-if="!userPermissions.canMerge">
{{
s__(
`mrWidget|Users who can write to the source or target branches can resolve the conflicts.`,
@@ -114,9 +86,9 @@ export default {
</span>
</template>
</template>
- <template v-if="!isLoading && !shouldBeRebased" #actions>
+ <template v-if="!isLoading && !state.shouldBeRebased" #actions>
<gl-button
- v-if="canMerge"
+ v-if="userPermissions.canMerge"
size="small"
variant="confirm"
category="secondary"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index 214d1b49732..5e073bf7c04 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -1,7 +1,6 @@
<script>
import { GlIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { sprintf } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import missingBranchQuery from '../../queries/states/missing_branch.query.graphql';
import {
@@ -21,13 +20,10 @@ export default {
GlSprintf,
StatusIcon,
},
- mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [mergeRequestQueryVariablesMixin],
apollo: {
state: {
query: missingBranchQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -44,15 +40,8 @@ export default {
return { state: {} };
},
computed: {
- sourceBranchRemoved() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return !this.state.sourceBranchExists;
- }
-
- return this.mr.sourceBranchRemoved;
- },
type() {
- return this.sourceBranchRemoved ? 'source' : 'target';
+ return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
name() {
return this.type === 'source' ? this.mr.sourceBranch : this.mr.targetBranch;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index f6843c1f3d3..4ae4edf02c3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -2,7 +2,6 @@
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
import simplePoll from '~/lib/utils/simple_poll';
import eventHub from '../../event_hub';
@@ -15,9 +14,6 @@ export default {
apollo: {
state: {
query: rebaseQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -29,7 +25,7 @@ export default {
GlButton,
StateContainer,
},
- mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
@@ -49,28 +45,16 @@ export default {
},
computed: {
isLoading() {
- return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
+ return this.$apollo.queries.state.loading;
},
rebaseInProgress() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.rebaseInProgress;
- }
-
- return this.mr.rebaseInProgress;
+ return this.state.rebaseInProgress;
},
canPushToSourceBranch() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.userPermissions.pushToSourceBranch;
- }
-
- return this.mr.canPushToSourceBranch;
+ return this.state.userPermissions.pushToSourceBranch;
},
targetBranch() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.targetBranch;
- }
-
- return this.mr.targetBranch;
+ return this.state.targetBranch;
},
status() {
if (this.isLoading) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
index 0b6aa104181..2db5c71be82 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
@@ -8,7 +8,7 @@ export default {
canMerge: {
query: readyToMergeQuery,
skip() {
- return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
+ return !this.mr;
},
variables() {
return this.mergeRequestQueryVariables;
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 1298c1316e2..c54672cd0f8 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
@@ -19,7 +19,6 @@ import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import simplePoll from '~/lib/utils/simple_poll';
import { __, s__, n__ } from '~/locale';
import SmartInterval from '~/smart_interval';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
AUTO_MERGE_STRATEGIES,
@@ -54,9 +53,6 @@ export default {
apollo: {
state: {
query: readyToMergeQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -123,14 +119,14 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [readyToMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [readyToMergeMixin, mergeRequestQueryVariablesMixin],
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
- loading: this.glFeatures.mergeRequestWidgetGraphql,
+ loading: true,
state: {},
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
isMakingRequest: false,
@@ -148,7 +144,7 @@ export default {
},
computed: {
stateData() {
- return this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
+ return this.state;
},
hasCI() {
return this.stateData.hasCI || this.stateData.hasCi;
@@ -157,35 +153,19 @@ export default {
return !isEmpty(this.stateData.availableAutoMergeStrategies);
},
pipeline() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.headPipeline;
- }
-
- return this.mr.pipeline;
+ return this.state.headPipeline;
},
isPipelineFailed() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return ['FAILED', 'CANCELED'].indexOf(this.pipeline?.status) !== -1;
- }
-
- return this.mr.isPipelineFailed;
+ return ['FAILED', 'CANCELED'].indexOf(this.pipeline?.status) !== -1;
},
showMergeFailedPipelineConfirmationDialog() {
return this.status === PIPELINE_FAILED_STATE && this.isPipelineFailed;
},
isMergeAllowed() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.mergeable;
- }
-
- return this.mr.isMergeAllowed;
+ return this.state.mergeable || false;
},
canRemoveSourceBranch() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.userPermissions.removeSourceBranch;
- }
-
- return this.mr.canRemoveSourceBranch;
+ return this.state.userPermissions.removeSourceBranch;
},
commitTemplateHelpPage() {
return helpPagePath('user/project/merge_requests/commit_templates.md');
@@ -200,46 +180,24 @@ export default {
return this.$options.i18n.mergeCommitTemplateHintText;
},
commits() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.commitsWithoutMergeCommits.nodes;
- }
-
- return this.mr.commits;
+ return this.state.commitsWithoutMergeCommits?.nodes;
},
commitsCount() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.state.commitCount || 0;
- }
-
- return this.mr.commitsCount;
+ return this.state.commitCount || 0;
},
preferredAutoMergeStrategy() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return MergeRequestStore.getPreferredAutoMergeStrategy(
- this.state.availableAutoMergeStrategies,
- );
- }
-
- return this.mr.preferredAutoMergeStrategy;
+ return MergeRequestStore.getPreferredAutoMergeStrategy(
+ this.state.availableAutoMergeStrategies,
+ );
},
squashIsSelected() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.isSquashReadOnly ? this.state.squashOnMerge : this.state.squash;
- }
-
- return this.mr.squashIsSelected;
+ return this.isSquashReadOnly ? this.state.squashOnMerge : this.state.squash;
},
isPipelineActive() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.pipeline?.active || false;
- }
-
- return this.mr.isPipelineActive;
+ return this.pipeline?.active || false;
},
status() {
- const ciStatus = this.glFeatures.mergeRequestWidgetGraphql
- ? this.pipeline?.status.toLowerCase()
- : this.mr.ciStatus;
+ const ciStatus = this.pipeline?.status?.toLowerCase();
if ((this.hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) {
return PIPELINE_FAILED_STATE;
@@ -304,11 +262,7 @@ export default {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
},
shouldShowMergeEdit() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return !this.state.mergeRequestsFfOnlyEnabled;
- }
-
- return !this.mr.ffOnlyEnabled;
+ return !this.state.mergeRequestsFfOnlyEnabled;
},
shaMismatchLink() {
return this.mr.mergeRequestDiffsPath;
@@ -345,18 +299,15 @@ export default {
},
},
mounted() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
- eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
- eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
- }
+ eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
+ eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
+ eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
},
beforeDestroy() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
- eventHub.$off('MRWidgetUpdateRequested', this.updateGraphqlState);
- eventHub.$off('mr.discussion.updated', this.updateGraphqlState);
- }
+ eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
+ eventHub.$off('MRWidgetUpdateRequested', this.updateGraphqlState);
+ eventHub.$off('mr.discussion.updated', this.updateGraphqlState);
+ eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
if (this.pollingInterval) {
this.pollingInterval.destroy();
@@ -391,9 +342,7 @@ export default {
if (mergeImmediately) {
this.isMergingImmediately = true;
}
- const latestSha = this.glFeatures.mergeRequestWidgetGraphql
- ? this.state.diffHeadSha
- : this.mr.latestSHA;
+ const latestSha = this.state.diffHeadSha;
const options = {
sha: latestSha || this.mr.sha,
@@ -435,9 +384,7 @@ export default {
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
}
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- this.updateGraphqlState();
- }
+ this.updateGraphqlState();
this.isMakingRequest = false;
})
@@ -521,7 +468,7 @@ export default {
<template>
<div
data-testid="ready_to_merge_state"
- class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7"
>
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
@@ -538,13 +485,15 @@ export default {
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
- <div class="gl-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-mb-5">
+ <div
+ class="gl-display-flex gl-sm-flex-direction-column gl-md-align-items-center gl-flex-wrap gl-w-full gl-md-pb-5"
+ >
<gl-form-checkbox
v-if="canRemoveSourceBranch"
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5"
+ class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5 gl-mb-3 gl-md-mb-0"
>
{{ __('Delete source branch') }}
</gl-form-checkbox>
@@ -555,37 +504,18 @@ export default {
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
- class="gl-mr-5"
+ class="gl-mr-5 gl-mb-3 gl-md-mb-0"
/>
<gl-form-checkbox
v-if="shouldShowSquashEdit || shouldShowMergeEdit"
v-model="editCommitMessage"
data-testid="widget_edit_commit_message"
- class="gl-display-flex gl-align-items-center"
+ class="gl-display-flex gl-align-items-center gl-mb-3 gl-md-mb-0"
>
{{ __('Edit commit message') }}
</gl-form-checkbox>
</div>
- <div class="gl-w-full gl-text-gray-500 gl-mb-5">
- <added-commit-message
- :is-squash-enabled="squashBeforeMerge"
- :is-fast-forward-enabled="!shouldShowMergeEdit"
- :commits-count="commitsCount"
- :target-branch="stateData.targetBranch"
- />
- <template v-if="mr.relatedLinks">
- &middot;
- <related-links
- :state="mr.state"
- :related-links="mr.relatedLinks"
- :show-assign-to-me="false"
- :diverged-commits-count="mr.divergedCommitsCount"
- :target-branch-path="mr.targetBranchPath"
- class="mr-ready-merge-related-links gl-display-inline"
- />
- </template>
- </div>
<div v-if="editCommitMessage" class="gl-w-full" data-testid="edit_commit_message">
<ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4">
<commit-edit
@@ -625,6 +555,25 @@ export default {
</li>
</ul>
</div>
+ <div class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5">
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="state.targetBranch"
+ />
+ <template v-if="mr.relatedLinks">
+ &middot;
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ :diverged-commits-count="mr.divergedCommitsCount"
+ :target-branch-path="mr.targetBranchPath"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </template>
+ </div>
<gl-button-group class="gl-align-self-start">
<gl-button
size="medium"
@@ -702,7 +651,7 @@ export default {
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
:commits-count="commitsCount"
- :target-branch="stateData.targetBranch"
+ :target-branch="state.targetBranch"
:merge-commit-path="mr.mergeCommitPath"
/>
</li>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 8f2e4eb2131..074758e33b2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -26,30 +26,30 @@ export default {
<template>
<state-container :mr="mr" status="failed">
<span
- class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body! gl-align-self-start"
>
{{ s__('mrWidget|Merge blocked: all threads must be resolved.') }}
</span>
<template #actions>
<gl-button
- v-if="mr.createIssueToResolveDiscussionsPath"
- :href="mr.createIssueToResolveDiscussionsPath"
- class="js-create-issue gl-align-self-start gl-vertical-align-top gl-mr-2"
+ data-testid="jump-to-first"
+ class="gl-align-self-start gl-vertical-align-top"
size="small"
variant="confirm"
- category="secondary"
+ category="primary"
+ @click="jumpToFirstUnresolvedDiscussion"
>
- {{ s__('mrWidget|Create issue to resolve all threads') }}
+ {{ s__('mrWidget|Jump to first unresolved thread') }}
</gl-button>
<gl-button
- data-testid="jump-to-first"
- class="gl-mb-2 gl-md-mb-0 gl-align-self-start gl-vertical-align-top"
+ v-if="mr.createIssueToResolveDiscussionsPath"
+ :href="mr.createIssueToResolveDiscussionsPath"
+ class="js-create-issue gl-align-self-start gl-vertical-align-top"
size="small"
variant="confirm"
- category="primary"
- @click="jumpToFirstUnresolvedDiscussion"
+ category="secondary"
>
- {{ s__('mrWidget|Jump to first unresolved thread') }}
+ {{ s__('mrWidget|Create issue to resolve all threads') }}
</gl-button>
</template>
</state-container>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index dee27a5d5b5..ef5be0fbfcd 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -5,14 +5,12 @@ import $ from 'jquery';
import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
-import MergeRequest from '~/merge_request';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql';
import draftQuery from '../../queries/states/draft.query.graphql';
import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
import StateContainer from '../state_container.vue';
+import eventHub from '../../event_hub';
export default {
name: 'WorkInProgress',
@@ -20,13 +18,10 @@ export default {
GlButton,
StateContainer,
},
- mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
+ mixins: [mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
query: draftQuery,
- skip() {
- return !this.glFeatures.mergeRequestWidgetGraphql;
- },
variables() {
return this.mergeRequestQueryVariables;
},
@@ -35,7 +30,6 @@ export default {
},
props: {
mr: { type: Object, required: true },
- service: { type: Object, required: true },
},
data() {
return {
@@ -43,17 +37,8 @@ export default {
isMakingRequest: false,
};
},
- computed: {
- canUpdate() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- return this.userPermissions.updateMergeRequest;
- }
-
- return Boolean(this.mr.removeWIPPath);
- },
- },
methods: {
- removeDraftMutation() {
+ handleRemoveDraft() {
const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true;
@@ -138,26 +123,6 @@ export default {
this.isMakingRequest = false;
});
},
- handleRemoveDraft() {
- if (this.glFeatures.mergeRequestWidgetGraphql) {
- this.removeDraftMutation();
- } else {
- this.isMakingRequest = true;
- this.service
- .removeWIP()
- .then((res) => res.data)
- .then((data) => {
- eventHub.$emit('UpdateWidgetData', data);
- MergeRequest.toggleDraftStatus(this.mr.title, true);
- })
- .catch(() => {
- this.isMakingRequest = false;
- createAlert({
- message: __('Something went wrong. Please try again.'),
- });
- });
- }
- },
},
};
</script>
@@ -169,12 +134,13 @@ export default {
</span>
<template #actions>
<gl-button
- v-if="canUpdate"
+ v-if="userPermissions.updateMergeRequest"
size="small"
:disabled="isMakingRequest"
:loading="isMakingRequest"
variant="confirm"
class="js-remove-draft gl-md-ml-3 gl-align-self-start"
+ data-testid="removeWipButton"
@click="handleRemoveDraft"
>
{{ s__('mrWidget|Mark as ready') }}
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 d1ade2886f4..4d66c75719b 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
@@ -58,6 +58,7 @@ export default {
:status-icon-name="statusIcon"
:widget-name="widgetName"
:header="data.header"
+ :help-popover="data.helpPopover"
>
<template #body>
<div class="gl-display-flex gl-flex-direction-column">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
index ff17de343d6..181b8cfad9a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
@@ -34,8 +34,8 @@ export default {
iconAriaLabel() {
return `${capitalizeFirstCharacter(this.iconName)} ${this.name}`;
},
- iconSize() {
- return this.level === 1 ? 16 : 12;
+ iconClassNameText() {
+ return this.$options.EXTENSION_ICON_CLASS[this.iconName];
},
},
EXTENSION_ICON_NAMES,
@@ -44,24 +44,22 @@ export default {
</script>
<template>
- <div :class="[$options.EXTENSION_ICON_CLASS[iconName]]" class="gl-mr-3">
+ <div
+ :class="{
+ [iconClassNameText]: !isLoading,
+ [`mr-widget-status-icon-level-${level}`]: !isLoading,
+ 'gl-mr-3': level === 1,
+ }"
+ class="gl-relative gl-w-6 gl-h-6 gl-rounded-full gl--flex-center"
+ >
<gl-loading-icon v-if="isLoading" size="md" inline />
- <div
+ <gl-icon
v-else
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-rounded-full gl-bg-gray-10"
- :class="{
- 'gl-p-2': level === 1,
- }"
- >
- <div class="gl-rounded-full gl-bg-white">
- <gl-icon
- :name="$options.EXTENSION_ICON_NAMES[iconName]"
- :size="iconSize"
- :aria-label="iconAriaLabel"
- :data-qa-selector="`status_${iconName}_icon`"
- class="gl-display-block"
- />
- </div>
- </div>
+ :name="$options.EXTENSION_ICON_NAMES[iconName]"
+ :size="12"
+ :aria-label="iconAriaLabel"
+ :data-qa-selector="`status_${iconName}_icon`"
+ class="gl-relative gl-z-index-1"
+ />
</div>
</template>
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 94359d7d6ac..cea7fb8260a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -1,29 +1,41 @@
<script>
-import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlButton,
+ GlLink,
+ GlTooltipDirective,
+ GlLoadingIcon,
+ GlSafeHtmlDirective,
+} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import ActionButtons from '../action_buttons.vue';
import { EXTENSION_ICONS } from '../../constants';
+import { createTelemetryHub } from '../extensions/telemetry';
import ContentRow from './widget_content_row.vue';
import DynamicContent from './dynamic_content.vue';
import StatusIcon from './status_icon.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
+const WIDGET_PREFIX = 'Widget';
export default {
components: {
ActionButtons,
StatusIcon,
+ GlLink,
GlButton,
GlLoadingIcon,
ContentRow,
DynamicContent,
+ HelpPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
/**
@@ -72,8 +84,8 @@ export default {
},
statusIconName: {
type: String,
- default: 'neutral',
required: false,
+ default: 'neutral',
validator: (value) => Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
},
isCollapsible: {
@@ -88,6 +100,26 @@ export default {
widgetName: {
type: String,
required: true,
+ // see https://docs.gitlab.com/ee/development/fe_guide/merge_request_widget_extensions.html#add-new-widgets
+ validator: (val) => val.startsWith(WIDGET_PREFIX),
+ },
+ telemetry: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ /**
+ * @typedef {Object} helpPopover
+ * @property {Object} options
+ * @property {String} options.title
+ * @property {Object} content
+ * @property {String} content.text
+ * @property {String} content.learnMorePath
+ */
+ helpPopover: {
+ type: Object,
+ required: false,
+ default: null,
},
},
data() {
@@ -98,6 +130,7 @@ export default {
isLoadingExpandedContent: false,
summaryError: null,
contentError: null,
+ telemetryHub: null,
};
},
computed: {
@@ -113,8 +146,14 @@ export default {
this.$emit('is-loading', newValue);
},
},
+ created() {
+ if (this.telemetry) {
+ this.telemetryHub = createTelemetryHub(this.widgetName);
+ }
+ },
async mounted() {
this.isLoading = true;
+ this.telemetryHub?.viewed();
try {
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
@@ -125,12 +164,21 @@ export default {
this.isLoading = false;
},
methods: {
+ onActionClick(action) {
+ if (action.fullReport) {
+ this.telemetryHub?.fullReportClicked();
+ }
+ },
toggleCollapsed() {
this.isCollapsed = !this.isCollapsed;
- if (this.isExpandedForTheFirstTime && typeof this.fetchExpandedData === 'function') {
- this.isExpandedForTheFirstTime = false;
- this.fetchExpandedContent();
+ if (this.isExpandedForTheFirstTime) {
+ this.telemetryHub?.expanded({ type: this.summaryStatusIcon });
+
+ if (typeof this.fetchExpandedData === 'function') {
+ this.isExpandedForTheFirstTime = false;
+ this.fetchExpandedContent();
+ }
}
},
async fetchExpandedContent() {
@@ -184,6 +232,9 @@ export default {
},
},
failedStatusIcon: EXTENSION_ICONS.failed,
+ i18n: {
+ learnMore: __('Learn more'),
+ },
};
</script>
@@ -204,11 +255,34 @@ export default {
<span v-if="summaryError">{{ summaryError }}</span>
<slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot>
</div>
- <action-buttons
- v-if="actionButtons.length > 0"
- :widget="widgetName"
- :tertiary-buttons="actionButtons"
- />
+ <div class="gl-display-flex">
+ <help-popover
+ v-if="helpPopover"
+ :options="helpPopover.options"
+ :class="{ 'gl-mr-3': actionButtons.length > 0 }"
+ >
+ <template v-if="helpPopover.content">
+ <p
+ v-if="helpPopover.content.text"
+ v-safe-html="helpPopover.content.text"
+ class="gl-mb-0"
+ ></p>
+ <gl-link
+ v-if="helpPopover.content.learnMorePath"
+ :href="helpPopover.content.learnMorePath"
+ target="_blank"
+ class="gl-font-sm"
+ >{{ $options.i18n.learnMore }}</gl-link
+ >
+ </template>
+ </help-popover>
+ <action-buttons
+ v-if="actionButtons.length > 0"
+ :widget="widgetName"
+ :tertiary-buttons="actionButtons"
+ @clickedAction="onActionClick"
+ />
+ </div>
<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"
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 ee81f0950a8..1fd1e325863 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
@@ -1,5 +1,8 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlSafeHtmlDirective, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import ActionButtons from '../action_buttons.vue';
import { EXTENSION_ICONS } from '../../constants';
import { generateText } from '../extensions/utils';
import StatusIcon from './status_icon.vue';
@@ -7,6 +10,9 @@ import StatusIcon from './status_icon.vue';
export default {
components: {
StatusIcon,
+ HelpPopover,
+ GlLink,
+ ActionButtons,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
@@ -19,8 +25,8 @@ export default {
},
statusIconName: {
type: String,
- default: '',
required: false,
+ default: '',
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
},
widgetName: {
@@ -29,8 +35,26 @@ export default {
},
header: {
type: [String, Array],
+ required: false,
default: '',
+ },
+ /**
+ * @typedef {Object} helpPopover
+ * @property {Object} options
+ * @property {String} options.title
+ * @property {Object} content
+ * @property {String} content.text
+ * @property {String} content.learnMorePath
+ */
+ helpPopover: {
+ type: Object,
required: false,
+ default: null,
+ },
+ actionButtons: {
+ type: Array,
+ required: false,
+ default: () => [],
},
},
computed: {
@@ -40,6 +64,12 @@ export default {
generatedSubheader() {
return Array.isArray(this.header) && this.header[1] ? generateText(this.header[1]) : '';
},
+ shouldShowHeaderActions() {
+ return Boolean(this.helpPopover) || this.actionButtons?.length > 0;
+ },
+ },
+ i18n: {
+ learnMore: __('Learn more'),
},
};
</script>
@@ -49,17 +79,46 @@ export default {
:class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
>
<status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" />
- <div>
- <slot name="header">
- <div v-if="header" class="gl-mb-2">
- <strong v-safe-html="generatedHeader" class="gl-display-block"></strong
- ><span
- v-if="generatedSubheader"
- v-safe-html="generatedSubheader"
- class="gl-display-block"
- ></span>
+ <div class="gl-w-full">
+ <div class="gl-display-flex">
+ <slot name="header">
+ <div v-if="header" class="gl-mb-2">
+ <strong v-safe-html="generatedHeader" class="gl-display-block"></strong
+ ><span
+ v-if="generatedSubheader"
+ v-safe-html="generatedSubheader"
+ class="gl-display-block"
+ ></span>
+ </div>
+ </slot>
+ <div
+ v-if="shouldShowHeaderActions"
+ class="gl-ml-auto gl-display-flex gl-align-items-baseline"
+ >
+ <help-popover v-if="helpPopover" :options="helpPopover.options">
+ <template v-if="helpPopover.content">
+ <p
+ v-if="helpPopover.content.text"
+ v-safe-html="helpPopover.content.text"
+ class="gl-mb-0"
+ ></p>
+ <gl-link
+ v-if="helpPopover.content.learnMorePath"
+ :href="helpPopover.content.learnMorePath"
+ target="_blank"
+ class="gl-font-sm"
+ >{{ $options.i18n.learnMore }}</gl-link
+ >
+ </template>
+ </help-popover>
+ <action-buttons
+ v-if="actionButtons.length > 0"
+ :widget="widgetName"
+ :tertiary-buttons="actionButtons"
+ :class="{ 'gl-ml-2': helpPopover }"
+ />
</div>
- </slot>
+ </div>
<div class="gl-display-flex gl-align-items-baseline gl-w-full">
<slot name="body"></slot>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index c6baf3b46ff..7109bed7743 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -192,4 +192,5 @@ export const DETAILED_MERGE_STATUS = {
POLICIES_DENIED: 'POLICIES_DENIED',
CI_MUST_PASS: 'CI_MUST_PASS',
CI_STILL_RUNNING: 'CI_STILL_RUNNING',
+ EXTERNAL_STATUS_CHECKS: 'EXTERNAL_STATUS_CHECKS',
};
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 a3f70b551bf..b96bdcb3833 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,7 +1,10 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
+import {
+ registerExtension,
+ registeredExtensions,
+} from '~/vue_merge_request_widget/components/extensions';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -47,6 +50,7 @@ import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
import codeQualityExtension from './extensions/code_quality';
import testReportExtension from './extensions/test_report';
+import ReportWidgetContainer from './components/report_widget_container.vue';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -82,21 +86,18 @@ export default {
MrWidgetAutoMergeFailed: AutoMergeFailed,
MrWidgetRebase: RebaseState,
SourceBranchRemovalStatus,
- GroupedCodequalityReportsApp: () =>
- import('../reports/codequality_report/grouped_codequality_reports_app.vue'),
- GroupedTestReportsApp: () =>
- import('../reports/grouped_test_report/grouped_test_reports_app.vue'),
MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
ReadyToMerge: ReadyToMergeState,
+ ReportWidgetContainer,
},
apollo: {
state: {
query: getStateQuery,
manual: true,
skip() {
- return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
+ return !this.mr;
},
variables() {
return this.mergeRequestQueryVariables;
@@ -130,13 +131,6 @@ export default {
};
},
computed: {
- isLoaded() {
- if (window.gon?.features?.mergeRequestWidgetGraphql) {
- return !this.loading;
- }
-
- return this.mr;
- },
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
},
@@ -185,9 +179,6 @@ export default {
shouldRenderTestReport() {
return Boolean(this.mr?.testResultsPath);
},
- shouldRenderRefactoredTestReport() {
- return window.gon?.features?.refactorMrWidgetTestSummary;
- },
mergeError() {
let { mergeError } = this.mr;
@@ -218,14 +209,14 @@ export default {
shouldShowSecurityExtension() {
return window.gon?.features?.refactorSecurityExtension;
},
- shouldShowCodeQualityExtension() {
- return window.gon?.features?.refactorCodeQualityExtension;
- },
shouldShowMergeDetails() {
if (this.mr.state === 'readyToMerge') return true;
return !this.mr.mergeDetailsCollapsed;
},
+ hasExtensions() {
+ return registeredExtensions.extensions.length;
+ },
},
watch: {
'mr.machineValue': {
@@ -343,9 +334,7 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
- if (window.gon?.features?.mergeRequestWidgetGraphql) {
- this.$apollo.queries.state.refetch();
- }
+ this.$apollo.queries.state.refetch();
return this.service
.checkStatus()
@@ -519,12 +508,12 @@ export default {
}
},
registerCodeQualityExtension() {
- if (this.shouldRenderCodeQuality && this.shouldShowCodeQualityExtension) {
+ if (this.shouldRenderCodeQuality) {
registerExtension(codeQualityExtension);
}
},
registerTestReportExtension() {
- if (this.shouldRenderTestReport && this.shouldRenderRefactoredTestReport) {
+ if (this.shouldRenderTestReport) {
registerExtension(testReportExtension);
}
},
@@ -532,7 +521,7 @@ export default {
};
</script>
<template>
- <div v-if="isLoaded" class="mr-state-widget gl-mt-3">
+ <div v-if="!loading" class="mr-state-widget gl-mt-3">
<header
v-if="shouldRenderCollaborationStatus"
class="gl-rounded-base gl-border-solid gl-border-1 gl-border-gray-100 gl-overflow-hidden mr-widget-workflow gl-mt-0!"
@@ -552,17 +541,19 @@ export default {
:user-callout-feature-id="mr.suggestPipelineFeatureId"
@dismiss="dismissSuggestPipelines"
/>
- <mr-widget-pipeline-container
- v-if="shouldRenderPipelines"
- class="mr-widget-workflow"
- :mr="mr"
- />
- <mr-widget-approvals
- v-if="shouldRenderApprovals"
- class="mr-widget-workflow"
- :mr="mr"
- :service="service"
- />
+ <mr-widget-pipeline-container v-if="shouldRenderPipelines" :mr="mr" />
+ <mr-widget-approvals v-if="shouldRenderApprovals" :mr="mr" :service="service" />
+ <report-widget-container>
+ <extensions-container v-if="hasExtensions" :mr="mr" />
+ <security-reports-app
+ v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension"
+ :pipeline-id="mr.pipeline.id"
+ :project-id="mr.sourceProjectId"
+ :security-reports-docs-path="mr.securityReportsDocsPath"
+ :target-project-full-path="mr.targetProjectFullPath"
+ :mr-iid="mr.iid"
+ />
+ </report-widget-container>
<div class="mr-section-container mr-widget-workflow">
<div v-if="hasAlerts" class="gl-overflow-hidden mr-widget-alert-container">
<mr-widget-alert-message
@@ -589,35 +580,8 @@ export default {
</mr-widget-alert-message>
</div>
- <extensions-container :mr="mr" />
-
<widget-container v-if="mr" :mr="mr" />
- <grouped-codequality-reports-app
- v-if="shouldRenderCodeQuality && !shouldShowCodeQualityExtension"
- :head-blob-path="mr.headBlobPath"
- :base-blob-path="mr.baseBlobPath"
- :codequality-reports-path="mr.codequalityReportsPath"
- :codequality-help-path="mr.codequalityHelpPath"
- />
-
- <security-reports-app
- v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension"
- :pipeline-id="mr.pipeline.id"
- :project-id="mr.sourceProjectId"
- :security-reports-docs-path="mr.securityReportsDocsPath"
- :target-project-full-path="mr.targetProjectFullPath"
- :mr-iid="mr.iid"
- />
-
- <grouped-test-reports-app
- v-if="shouldRenderTestReport && !shouldRenderRefactoredTestReport"
- class="js-reports-container"
- :endpoint="mr.testResultsPath"
- :head-blob-path="mr.headBlobPath"
- :pipeline-path="mr.pipeline.path"
- />
-
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
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 731d3886f61..86ce032ea3d 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
@@ -29,6 +29,7 @@ export default class MergeRequestStore {
this.stateMachine = machine(STATE_MACHINE.definition);
this.machineValue = this.stateMachine.value;
this.mergeDetailsCollapsed = window.innerWidth < 768;
+ this.mergeError = data.mergeError;
this.setPaths(data);
@@ -157,25 +158,6 @@ export default class MergeRequestStore {
this.mergeCommitPath = data.merged_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
- if (!window.gon?.features?.mergeRequestWidgetGraphql) {
- this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
- this.canBeMerged = data.can_be_merged || false;
- this.canMerge = Boolean(data.merge_path);
- this.commitsCount = data.commits_count;
- this.branchMissing = data.branch_missing;
- this.hasConflicts = data.has_conflicts;
- this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
- this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
- this.mergeError = data.merge_error;
- this.mergeStatus = data.merge_status;
- this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
- this.allowMergeOnSkippedPipeline = data.allow_merge_on_skipped_pipeline || false;
- this.projectArchived = data.project_archived;
- this.isSHAMismatch = this.sha !== data.diff_head_sha;
- this.shouldBeRebased = Boolean(data.should_be_rebased);
- this.draft = data.draft;
- }
-
const currentUser = data.current_user;
this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
@@ -299,7 +281,6 @@ export default class MergeRequestStore {
this.headBlobPath = blobPath.head_path || '';
this.baseBlobPath = blobPath.base_path || '';
this.codequalityReportsPath = data.codequality_reports_path;
- this.codequalityHelpPath = data.codequality_help_path;
// Security reports
this.sastComparisonPath = data.sast_comparison_path;
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index f2ea55df63d..96c2ffa929c 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -145,11 +145,14 @@ export default {
},
currentTabIndex: {
get() {
- return this.$options.tabsConfig.findIndex((tab) => tab.id === this.activeTab);
+ const tabIndex = this.$options.tabsConfig.findIndex((tab) => tab.id === this.activeTab);
+ return tabIndex >= 0 ? tabIndex : 0;
},
set(tabIdx) {
const tabId = this.$options.tabsConfig[tabIdx].id;
- this.$router.replace({ name: 'tab', params: { tabId } });
+ if (this.$route.params?.tabId !== tabId) {
+ this.$router.push({ name: 'tab', params: { tabId } });
+ }
},
},
environmentName() {
diff --git a/app/assets/javascripts/vue_shared/alert_details/index.js b/app/assets/javascripts/vue_shared/alert_details/index.js
index 5793069440c..357dfa49901 100644
--- a/app/assets/javascripts/vue_shared/alert_details/index.js
+++ b/app/assets/javascripts/vue_shared/alert_details/index.js
@@ -15,9 +15,17 @@ Vue.use(VueApollo);
export default (selector) => {
const domEl = document.querySelector(selector);
- const { alertId, projectPath, projectIssuesPath, projectId, page, canUpdate } = domEl.dataset;
+ const {
+ alertId,
+ projectPath,
+ projectIssuesPath,
+ projectAlertManagementDetailsPath,
+ projectId,
+ page,
+ canUpdate,
+ } = domEl.dataset;
const iid = alertId;
- const router = createRouter();
+ const router = createRouter(projectAlertManagementDetailsPath);
const resolvers = {
Mutation: {
diff --git a/app/assets/javascripts/vue_shared/alert_details/router.js b/app/assets/javascripts/vue_shared/alert_details/router.js
index 5687fe4e0f5..26477a3a66a 100644
--- a/app/assets/javascripts/vue_shared/alert_details/router.js
+++ b/app/assets/javascripts/vue_shared/alert_details/router.js
@@ -5,9 +5,26 @@ import { joinPaths } from '~/lib/utils/url_utility';
Vue.use(VueRouter);
export default function createRouter(base) {
- return new VueRouter({
- mode: 'hash',
+ const router = new VueRouter({
+ mode: 'history',
base: joinPaths(gon.relative_url_root || '', base),
routes: [{ path: '/:tabId', name: 'tab' }],
});
+
+ /*
+ Backward-compatible behavior. Redirects hash mode URLs to history mode ones.
+ Ex: from #/overview to /overview
+ from #/metrics to /metrics
+ from #/activity to /activity
+ */
+ router.beforeEach((to, _, next) => {
+ if (to.hash.startsWith('#/')) {
+ const path = to.fullPath.substring(2);
+ next(path);
+ } else {
+ next();
+ }
+ });
+
+ return router;
}
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 dc4d1bd56e9..ed0eb9cc0b8 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
@@ -15,8 +15,14 @@ export default {
mounted() {
handleBlobRichViewer(this.$refs.content, this.type);
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['copy-code'],
+ },
};
</script>
<template>
- <markdown-field-view ref="content" v-safe-html="richViewer || content" />
+ <markdown-field-view
+ ref="content"
+ v-safe-html:[$options.safeHtmlConfig]="richViewer || content"
+ />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/code_block.stories.js b/app/assets/javascripts/vue_shared/components/code_block.stories.js
index e02a346c1de..994913dc1a8 100644
--- a/app/assets/javascripts/vue_shared/components/code_block.stories.js
+++ b/app/assets/javascripts/vue_shared/components/code_block.stories.js
@@ -13,6 +13,5 @@ const Template = (args, { argTypes }) => ({
export const Default = Template.bind({});
Default.args = {
- // eslint-disable-next-line @gitlab/require-i18n-strings
code: `git commit -a "Message"\ngit push`,
};
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index 5b9efff1c06..2bdc8a174d0 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -36,6 +36,11 @@ export default {
required: false,
default: 'confirm-danger-button',
},
+ buttonQaSelector: {
+ type: String,
+ required: false,
+ default: null,
+ },
buttonVariant: {
type: String,
required: false,
@@ -53,7 +58,7 @@ export default {
:variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
- data-qa-selector="confirm_danger_button"
+ :data-qa-selector="buttonQaSelector"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
index 7ecc309db52..b56434f746e 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import ConfirmDanger from './confirm_danger.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
index 8256d953466..a48b8bcfa8e 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.stories.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import { __ } from '~/locale';
import DropdownWidget from './dropdown_widget.vue';
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 2227047a909..8a3a174f414 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -45,7 +45,7 @@ export default {
},
levelIndentation() {
return {
- marginLeft: this.level ? `${this.level * 16}px` : null,
+ marginLeft: this.level ? `${this.level * 8}px` : null,
};
},
fileClass() {
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 4873996d357..755ce004aa9 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
@@ -13,11 +13,19 @@ export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
export const OPERATOR_IS_NOT = '!=';
-export const OPERATOR_IS_NOT_TEXT = __('is not');
+export const OPERATOR_IS_NOT_TEXT = __('is not one of');
+export const OPERATOR_OR = '||';
+export const OPERATOR_OR_TEXT = __('is one of');
export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }];
+export const OPERATOR_OR_ONLY = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY];
+export const OPERATOR_IS_NOT_OR = [
+ ...OPERATOR_IS_ONLY,
+ ...OPERATOR_IS_NOT_ONLY,
+ ...OPERATOR_OR_ONLY,
+];
export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
@@ -55,10 +63,26 @@ export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
export const TOKEN_TITLE_RELEASE = __('Release');
+export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
+export const TOKEN_TITLE_STATUS = __('Status');
+export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TYPE_ASSIGNEE = 'assignee';
+export const TOKEN_TYPE_AUTHOR = 'author';
+export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
+export const TOKEN_TYPE_CONTACT = 'contact';
+export const TOKEN_TYPE_EPIC = 'epic';
// As health status gets reused between issue lists and boards
// this is in the shared constants. Until we have not decoupled the EE filtered search bar
// from the CE component, we need to keep this in the CE code.
// https://gitlab.com/gitlab-org/gitlab/-/issues/377838
-export const TOKEN_TYPE_HEALTH = 'health_status';
+export const TOKEN_TYPE_HEALTH = 'health';
+export const TOKEN_TYPE_ITERATION = 'iteration';
+export const TOKEN_TYPE_LABEL = 'label';
+export const TOKEN_TYPE_MILESTONE = 'milestone';
+export const TOKEN_TYPE_MY_REACTION = 'my-reaction';
+export const TOKEN_TYPE_ORGANIZATION = 'organization';
+export const TOKEN_TYPE_RELEASE = 'release';
+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 8821084ef35..0d0787e7033 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
@@ -89,6 +89,11 @@ export default {
required: false,
default: () => ({}),
},
+ showFriendlyText: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
syncFilterAndSort: {
type: Boolean,
required: false,
@@ -319,7 +324,7 @@ export default {
(sortBy) =>
sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort,
);
- this.selectedSortDirection = Object.keys(this.selectedSortOption.sortDirection).find(
+ this.selectedSortDirection = Object.keys(this.selectedSortOption?.sortDirection || {}).find(
(key) => this.selectedSortOption.sortDirection[key] === sort,
);
},
@@ -351,6 +356,7 @@ export default {
:close-button-title="__('Close')"
:clear-recent-searches-text="__('Clear recent searches')"
:no-recent-searches-text="__(`You don't have any recent searches`)"
+ :show-friendly-text="showFriendlyText"
class="flex-grow-1"
@history-item-selected="handleHistoryItemSelected"
@clear="onClear"
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index 482a2964b4c..2f10e068542 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -129,6 +129,8 @@ export default {
v-gl-tooltip.hover="toggleVisibilityLabel"
:aria-label="toggleVisibilityLabel"
:icon="toggleVisibilityIcon"
+ data-testid="toggle-visibility-button"
+ data-qa-selector="toggle_visibility_button"
@click.stop="handleToggleVisibilityButtonClick"
/>
<clipboard-button
diff --git a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
deleted file mode 100644
index c2be5e4f7a1..00000000000
--- a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
-import { GlBadge } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-import axios from '~/lib/utils/axios_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-const STATUS_TYPES = {
- SUCCESS: 'success',
- WARNING: 'warning',
- DANGER: 'danger',
-};
-
-const UPGRADE_DOCS_URL = helpPagePath('update/index');
-
-export default {
- name: 'GitlabVersionCheck',
- components: {
- GlBadge,
- },
- mixins: [Tracking.mixin()],
- props: {
- size: {
- type: String,
- required: false,
- default: 'md',
- },
- },
- data() {
- return {
- status: null,
- };
- },
- computed: {
- title() {
- if (this.status === STATUS_TYPES.SUCCESS) {
- return s__('VersionCheck|Up to date');
- } else if (this.status === STATUS_TYPES.WARNING) {
- return s__('VersionCheck|Update available');
- } else if (this.status === STATUS_TYPES.DANGER) {
- return s__('VersionCheck|Update ASAP');
- }
-
- return null;
- },
- },
- created() {
- this.checkGitlabVersion();
- },
- methods: {
- checkGitlabVersion() {
- axios
- .get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
- .then((res) => {
- if (res.data) {
- this.status = res.data.severity;
-
- this.track('rendered_version_badge', {
- label: this.title,
- });
- }
- })
- .catch(() => {
- // Silently fail
- this.status = null;
- });
- },
- onClick() {
- this.track('click_version_badge', { label: this.title });
- },
- },
- UPGRADE_DOCS_URL,
-};
-</script>
-
-<template>
- <!-- TODO: remove the span element once bootstrap-vue is updated to version 2.21.1 -->
- <!-- TODO: https://github.com/bootstrap-vue/bootstrap-vue/issues/6219 -->
- <span v-if="status" data-testid="badge-click-wrapper" @click="onClick">
- <gl-badge
- :href="$options.UPGRADE_DOCS_URL"
- class="version-check-badge"
- :variant="status"
- :size="size"
- >{{ title }}</gl-badge
- >
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/group_select/constants.js
new file mode 100644
index 00000000000..bc70936eb36
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/constants.js
@@ -0,0 +1,6 @@
+import { __ } from '~/locale';
+
+export const TOGGLE_TEXT = __('Search for a group');
+export const FETCH_GROUPS_ERROR = __('Unable to fetch groups. Reload the page to try again.');
+export const FETCH_GROUP_ERROR = __('Unable to fetch group. Reload the page to try again.');
+export const QUERY_TOO_SHORT_MESSAGE = __('Enter at least three characters to search.');
diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
new file mode 100644
index 00000000000..1de6c0121bc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
@@ -0,0 +1,195 @@
+<script>
+import { debounce } from 'lodash';
+import { GlListbox } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import Api from '~/api';
+import { __ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { createAlert } from '~/flash';
+import { groupsPath } from './utils';
+import {
+ TOGGLE_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+ QUERY_TOO_SHORT_MESSAGE,
+} from './constants';
+
+const MINIMUM_QUERY_LENGTH = 3;
+
+export default {
+ components: {
+ GlListbox,
+ },
+ props: {
+ inputName: {
+ type: String,
+ required: true,
+ },
+ inputId: {
+ type: String,
+ required: true,
+ },
+ initialSelection: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ clearable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ parentGroupID: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ groupsFilter: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ pristine: true,
+ searching: false,
+ searchString: '',
+ groups: [],
+ selectedValue: null,
+ selectedText: null,
+ };
+ },
+ computed: {
+ selected: {
+ set(value) {
+ this.selectedValue = value;
+ this.selectedText =
+ value === null ? null : this.groups.find((group) => group.value === value).full_name;
+ },
+ get() {
+ return this.selectedValue;
+ },
+ },
+ toggleText() {
+ return this.selectedText ?? this.$options.i18n.toggleText;
+ },
+ inputValue() {
+ return this.selectedValue ? this.selectedValue : '';
+ },
+ isSearchQueryTooShort() {
+ return this.searchString && this.searchString.length < MINIMUM_QUERY_LENGTH;
+ },
+ noResultsText() {
+ return this.isSearchQueryTooShort
+ ? this.$options.i18n.searchQueryTooShort
+ : this.$options.i18n.noResultsText;
+ },
+ },
+ created() {
+ this.fetchInitialSelection();
+ },
+ methods: {
+ search: debounce(function debouncedSearch(searchString) {
+ this.searchString = searchString;
+ if (this.isSearchQueryTooShort) {
+ this.groups = [];
+ } else {
+ this.fetchGroups(searchString);
+ }
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
+ async fetchGroups(searchString = '') {
+ this.searching = true;
+
+ try {
+ const { data } = await axios.get(
+ Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
+ {
+ params: {
+ search: searchString,
+ },
+ },
+ );
+ const groups = data.length ? data : data.results || [];
+
+ this.groups = groups.map((group) => ({
+ ...group,
+ value: String(group.id),
+ }));
+
+ this.searching = false;
+ } catch (error) {
+ createAlert({
+ message: FETCH_GROUPS_ERROR,
+ error,
+ parent: this.$el,
+ });
+ }
+ },
+ async fetchInitialSelection() {
+ if (!this.initialSelection) {
+ this.pristine = false;
+ return;
+ }
+ this.searching = true;
+ try {
+ const group = await Api.group(this.initialSelection);
+ this.selectedValue = this.initialSelection;
+ this.selectedText = group.full_name;
+ this.pristine = false;
+ this.searching = false;
+ } catch (error) {
+ createAlert({
+ message: FETCH_GROUP_ERROR,
+ error,
+ parent: this.$el,
+ });
+ }
+ },
+ onShown() {
+ if (!this.searchString && !this.groups.length) {
+ this.fetchGroups();
+ }
+ },
+ onReset() {
+ this.selected = null;
+ },
+ },
+ i18n: {
+ toggleText: TOGGLE_TEXT,
+ selectGroup: __('Select a group'),
+ reset: __('Reset'),
+ noResultsText: __('No results found.'),
+ searchQueryTooShort: QUERY_TOO_SHORT_MESSAGE,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-listbox
+ ref="listbox"
+ v-model="selected"
+ :header-text="$options.i18n.selectGroup"
+ :reset-button-label="$options.i18n.reset"
+ :toggle-text="toggleText"
+ :loading="searching && pristine"
+ :searching="searching"
+ :items="groups"
+ :no-results-text="noResultsText"
+ searchable
+ @shown="onShown"
+ @search="search"
+ @reset="onReset"
+ >
+ <template #list-item="{ item }">
+ <div class="gl-font-weight-bold">
+ {{ item.full_name }}
+ </div>
+ <div class="gl-text-gray-300">{{ item.full_path }}</div>
+ </template>
+ </gl-listbox>
+ <div class="flash-container"></div>
+ <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index 1b89bd324c6..f349aa78bac 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -20,6 +20,11 @@ export default {
required: false,
default: () => ({}),
},
+ icon: {
+ type: String,
+ required: false,
+ default: 'question-o',
+ },
},
methods: {
targetFn() {
@@ -30,7 +35,7 @@ export default {
</script>
<template>
<span>
- <gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
+ <gl-button ref="popoverTrigger" variant="link" :icon="icon" :aria-label="__('Help')" />
<gl-popover :target="targetFn" v-bind="options">
<template v-if="options.title" #title>
<span v-safe-html="options.title"></span>
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 b38772d5aa5..c0712e46613 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -72,7 +72,7 @@ export default {
required: false,
default: '',
},
- initOnAutofocus: {
+ autofocus: {
type: Boolean,
required: false,
default: false,
@@ -87,20 +87,20 @@ export default {
return {
editingMode: EDITING_MODE_MARKDOWN_FIELD,
switchEditingControlEnabled: true,
- autofocus: this.initOnAutofocus,
+ autofocused: false,
};
},
computed: {
isContentEditorActive() {
return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
},
- contentEditorAutofocus() {
+ contentEditorAutofocused() {
// Match textarea focus behavior
- return this.autofocus ? 'end' : false;
+ return this.autofocus && !this.autofocused ? 'end' : false;
},
},
mounted() {
- this.autofocusTextarea(this.editingMode);
+ this.autofocusTextarea();
},
methods: {
updateMarkdownFromContentEditor({ markdown }) {
@@ -120,7 +120,6 @@ export default {
},
onEditingModeChange(editingMode) {
this.notifyEditingModeChange(editingMode);
- this.enableAutofocus(editingMode);
},
onEditingModeRestored(editingMode) {
this.notifyEditingModeChange(editingMode);
@@ -128,15 +127,15 @@ export default {
notifyEditingModeChange(editingMode) {
this.$emit(editingMode);
},
- enableAutofocus(editingMode) {
- this.autofocus = true;
- this.autofocusTextarea(editingMode);
- },
- autofocusTextarea(editingMode) {
- if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) {
+ autofocusTextarea() {
+ if (this.autofocus && this.editingMode === EDITING_MODE_MARKDOWN_FIELD) {
this.$refs.textarea.focus();
+ this.setEditorAsAutofocused();
}
},
+ setEditorAsAutofocused() {
+ this.autofocused = true;
+ },
},
switchEditingControlOptions: [
{ text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
@@ -197,7 +196,8 @@ export default {
:render-markdown="renderMarkdown"
:uploads-path="uploadsPath"
:markdown="value"
- :autofocus="contentEditorAutofocus"
+ :autofocus="contentEditorAutofocused"
+ @initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
@loading="disableSwitchEditingControl"
@loadingSuccess="enableSwitchEditingControl"
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js
new file mode 100644
index 00000000000..03bd64e2a57
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js
@@ -0,0 +1,54 @@
+import { GlButton } from '@gitlab/ui';
+import { MOCK_HTML } from '../../../../../../spec/frontend/vue_shared/components/markdown_drawer/mock_data';
+import MarkdownDrawer from './markdown_drawer.vue';
+
+export default {
+ component: MarkdownDrawer,
+ title: 'vue_shared/markdown_drawer',
+ parameters: {
+ mirage: {
+ timing: 1000,
+ handlers: {
+ get: {
+ '/help/user/search/global_search/advanced_search_syntax.json': [
+ 200,
+ {},
+ { html: MOCK_HTML },
+ ],
+ },
+ },
+ },
+ },
+};
+
+const createStory = ({ ...options }) => (_, { argTypes }) => ({
+ components: { MarkdownDrawer, GlButton },
+ props: Object.keys(argTypes),
+ data() {
+ return {
+ render: false,
+ };
+ },
+ methods: {
+ toggleDrawer() {
+ this.$refs.drawer.toggleDrawer();
+ },
+ },
+ mounted() {
+ window.requestAnimationFrame(() => {
+ this.render = true;
+ });
+ },
+ template: `
+ <div v-if="render">
+ <gl-button @click="toggleDrawer">Open Drawer</gl-button>
+ <markdown-drawer
+ :documentPath="'user/search/global_search/advanced_search_syntax.json'"
+ ref="drawer"
+ />
+ </div>
+ `,
+ ...options,
+});
+
+export const Default = createStory({});
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
new file mode 100644
index 00000000000..a4b509f8656
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
@@ -0,0 +1,117 @@
+<script>
+import { GlSafeHtmlDirective as SafeHtml, GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
+import { s__ } from '~/locale';
+import { contentTop } from '~/lib/utils/common_utils';
+import { getRenderedMarkdown } from './utils/fetch';
+
+export const cache = {};
+
+export default {
+ name: 'MarkdownDrawer',
+ components: {
+ GlDrawer,
+ GlAlert,
+ GlSkeletonLoader,
+ },
+ directives: {
+ SafeHtml,
+ },
+ i18n: {
+ alert: s__('MardownDrawer|Could not fetch help contents.'),
+ },
+ props: {
+ documentPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ hasFetchError: false,
+ title: '',
+ body: null,
+ open: false,
+ };
+ },
+ computed: {
+ drawerOffsetTop() {
+ return `${contentTop()}px`;
+ },
+ },
+ watch: {
+ documentPath: {
+ immediate: true,
+ handler: 'fetchMarkdown',
+ },
+ open(open) {
+ if (open && this.body) {
+ this.renderGLFM();
+ }
+ },
+ },
+ methods: {
+ async fetchMarkdown() {
+ const cached = cache[this.documentPath];
+ this.hasFetchError = false;
+ this.title = '';
+ if (cached) {
+ this.title = cached.title;
+ this.body = cached.body;
+ if (this.open) {
+ this.renderGLFM();
+ }
+ } else {
+ this.loading = true;
+ const { body, title, hasFetchError } = await getRenderedMarkdown(this.documentPath);
+ this.title = title;
+ this.body = body;
+ this.loading = false;
+ this.hasFetchError = hasFetchError;
+ if (this.open) {
+ this.renderGLFM();
+ }
+ cache[this.documentPath] = { title, body };
+ }
+ },
+ renderGLFM() {
+ this.$nextTick(() => {
+ $(this.$refs['content-element']).renderGFM();
+ });
+ },
+ closeDrawer() {
+ this.open = false;
+ },
+ toggleDrawer() {
+ this.open = !this.open;
+ },
+ openDrawer() {
+ this.open = true;
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['copy-code'],
+ },
+};
+</script>
+<template>
+ <gl-drawer :header-height="drawerOffsetTop" :open="open" header-sticky @close="closeDrawer">
+ <template #title>
+ <h4 data-testid="title-element" class="gl-m-0">{{ title }}</h4>
+ </template>
+ <template #default>
+ <div v-if="hasFetchError">
+ <gl-alert :dismissible="false" variant="danger">{{ $options.i18n.alert }}</gl-alert>
+ </div>
+ <gl-skeleton-loader v-else-if="loading" />
+ <div
+ v-else
+ ref="content-element"
+ v-safe-html:[$options.safeHtmlConfig]="body"
+ class="md"
+ ></div>
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
new file mode 100644
index 00000000000..7c8e1bc160a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -0,0 +1,32 @@
+import * as Sentry from '@sentry/browser';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import axios from '~/lib/utils/axios_utils';
+
+export const splitDocument = (htmlString) => {
+ const htmlDocument = new DOMParser().parseFromString(htmlString, 'text/html');
+ const title = htmlDocument.querySelector('h1')?.innerText;
+ htmlDocument.querySelector('h1')?.remove();
+ return {
+ title,
+ body: htmlDocument.querySelector('body').innerHTML.toString(),
+ };
+};
+
+export const getRenderedMarkdown = (documentPath) => {
+ return axios
+ .get(helpPagePath(documentPath))
+ .then(({ data }) => {
+ const { body, title } = splitDocument(data.html);
+ return {
+ body,
+ title,
+ hasFetchError: false,
+ };
+ })
+ .catch((e) => {
+ Sentry.captureException(e);
+ return {
+ hasFetchError: true,
+ };
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
deleted file mode 100644
index ba9edc7620a..00000000000
--- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
+++ /dev/null
@@ -1,212 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export const EMPTY_NAMESPACE_ID = -1;
-export const i18n = {
- DEFAULT_TEXT: __('Select a new namespace'),
- DEFAULT_EMPTY_NAMESPACE_TEXT: __('No namespace'),
- GROUPS: __('Groups'),
- USERS: __('Users'),
-};
-
-const filterByName = (data, searchTerm = '') => {
- if (!searchTerm) {
- return data;
- }
-
- return data.filter((d) => d.humanName.toLowerCase().includes(searchTerm.toLowerCase()));
-};
-
-export default {
- name: 'NamespaceSelectDeprecated',
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlIntersectionObserver,
- GlLoadingIcon,
- },
- props: {
- groupNamespaces: {
- type: Array,
- required: false,
- default: () => [],
- },
- userNamespaces: {
- type: Array,
- required: false,
- default: () => [],
- },
- fullWidth: {
- type: Boolean,
- required: false,
- default: false,
- },
- defaultText: {
- type: String,
- required: false,
- default: i18n.DEFAULT_TEXT,
- },
- includeHeaders: {
- type: Boolean,
- required: false,
- default: true,
- },
- emptyNamespaceTitle: {
- type: String,
- required: false,
- default: i18n.DEFAULT_EMPTY_NAMESPACE_TEXT,
- },
- includeEmptyNamespace: {
- type: Boolean,
- required: false,
- default: false,
- },
- hasNextPageOfGroups: {
- type: Boolean,
- required: false,
- default: false,
- },
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- isSearchLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldFilterNamespaces: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- data() {
- return {
- searchTerm: '',
- selectedNamespace: null,
- };
- },
- computed: {
- hasUserNamespaces() {
- return this.userNamespaces.length;
- },
- hasGroupNamespaces() {
- return this.groupNamespaces.length;
- },
- filteredGroupNamespaces() {
- if (!this.shouldFilterNamespaces) return this.groupNamespaces;
- if (!this.hasGroupNamespaces) return [];
- return filterByName(this.groupNamespaces, this.searchTerm);
- },
- filteredUserNamespaces() {
- if (!this.shouldFilterNamespaces) return this.userNamespaces;
- if (!this.hasUserNamespaces) return [];
- return filterByName(this.userNamespaces, this.searchTerm);
- },
- selectedNamespaceText() {
- return this.selectedNamespace?.humanName || this.defaultText;
- },
- filteredEmptyNamespaceTitle() {
- const { includeEmptyNamespace, emptyNamespaceTitle, searchTerm } = this;
-
- if (!includeEmptyNamespace) {
- return '';
- }
- if (!searchTerm) {
- return emptyNamespaceTitle;
- }
-
- return emptyNamespaceTitle.toLowerCase().includes(searchTerm.toLowerCase());
- },
- },
- watch: {
- searchTerm() {
- this.$emit('search', this.searchTerm);
- },
- },
- methods: {
- handleSelect(item) {
- this.selectedNamespace = item;
- this.searchTerm = '';
- this.$emit('select', item);
- },
- handleSelectEmptyNamespace() {
- this.handleSelect({ id: EMPTY_NAMESPACE_ID, humanName: this.emptyNamespaceTitle });
- },
- },
- i18n,
-};
-</script>
-<template>
- <gl-dropdown
- :text="selectedNamespaceText"
- :block="fullWidth"
- data-qa-selector="namespaces_list"
- @show="$emit('show')"
- >
- <template #header>
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- :is-loading="isSearchLoading"
- data-qa-selector="namespaces_list_search"
- />
- </template>
- <div v-if="filteredEmptyNamespaceTitle">
- <gl-dropdown-item
- data-qa-selector="namespaces_list_item"
- @click="handleSelectEmptyNamespace()"
- >
- {{ emptyNamespaceTitle }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- </div>
- <div
- v-if="hasUserNamespaces"
- data-qa-selector="namespaces_list_users"
- data-testid="namespace-list-users"
- >
- <gl-dropdown-section-header v-if="includeHeaders">{{
- $options.i18n.USERS
- }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="item in filteredUserNamespaces"
- :key="item.id"
- data-qa-selector="namespaces_list_item"
- @click="handleSelect(item)"
- >{{ item.humanName }}</gl-dropdown-item
- >
- </div>
- <div
- v-if="hasGroupNamespaces"
- data-qa-selector="namespaces_list_groups"
- data-testid="namespace-list-groups"
- >
- <gl-dropdown-section-header v-if="includeHeaders">{{
- $options.i18n.GROUPS
- }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="item in filteredGroupNamespaces"
- :key="item.id"
- data-qa-selector="namespaces_list_item"
- @click="handleSelect(item)"
- >{{ item.humanName }}</gl-dropdown-item
- >
- </div>
- <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
- <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')" />
- </gl-dropdown>
-</template>
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 a5027d2ca5c..867222279b2 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
@@ -9,9 +9,12 @@ import {
} from '@gitlab/ui';
import Api from '~/api';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
import Tracking from '~/tracking';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ OPERATOR_IS_ONLY,
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
@@ -112,7 +115,7 @@ export default {
{
type: 'author_username',
icon: 'user',
- title: __('Author'),
+ title: TOKEN_TITLE_AUTHOR,
unique: true,
symbol: '@',
token: AuthorToken,
@@ -123,7 +126,7 @@ export default {
{
type: 'assignee_username',
icon: 'user',
- title: __('Assignee'),
+ title: TOKEN_TITLE_ASSIGNEE,
unique: true,
symbol: '@',
token: AuthorToken,
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
index f16afc77164..fd9d69bae22 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.stories.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import PaginationBar from './pagination_bar.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js
deleted file mode 100644
index 1c08433ee78..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/epics_select/epics_select_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js
deleted file mode 100644
index 1c08433ee78..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/health_status_select/health_status_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
index 0f5560ff628..02323e5a0c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
@@ -43,6 +43,11 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -128,7 +133,7 @@ export default {
</script>
<template>
- <div class="block js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
+ <div class="js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown">
<div
v-gl-tooltip.left.viewport
data-testid="move-collapsed"
@@ -141,7 +146,7 @@ export default {
<gl-dropdown
ref="dropdown"
:block="true"
- :disabled="moveInProgress"
+ :disabled="moveInProgress || disabled"
class="hide-collapsed"
toggle-class="js-sidebar-dropdown-toggle"
@shown="fetchProjects"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js b/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js
deleted file mode 100644
index 1c08433ee78..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/iterations_dropdown_bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-// This empty file satisfies the import/no-unresolved rule for ee_else_ce imports.
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 0127df730b8..27186281c42 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -194,6 +194,7 @@ export default {
ref="dropdown"
:text="buttonText"
class="gl-w-full"
+ block
data-testid="labels-select-dropdown-contents"
data-qa-selector="labels_dropdown_content"
@hide="handleDropdownHide"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
index caeee2df7e5..314ffbaf84c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center gl-word-break-word">
<span
class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
:style="{ 'background-color': label.color }"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 0e8da7281d8..2c27a69d587 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -129,9 +129,6 @@ export default {
issuableId() {
return this.issuable?.id;
},
- isRealtimeEnabled() {
- return this.glFeatures.realtimeLabels;
- },
},
apollo: {
issuable: {
@@ -163,7 +160,7 @@ export default {
};
},
skip() {
- return !this.issuableId || !this.isDropdownVariantSidebar || !this.isRealtimeEnabled;
+ return !this.issuableId || !this.isDropdownVariantSidebar;
},
updateQuery(
_,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
new file mode 100644
index 00000000000..a1b16b378b3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
@@ -0,0 +1,22 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
+
+subscription mergeRequestReviewersUpdated($issuableId: IssuableID!) {
+ mergeRequestReviewersUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ id
+ reviewers {
+ nodes {
+ ...User
+ ...UserAvailability
+ mergeRequestInteraction {
+ canMerge
+ canUpdate
+ approved
+ reviewed
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
index 8a2bab4cb9a..465ee9aa0d4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import TodoButton from './todo_button.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
index 9683288f937..a2d8b7cbd15 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -1,5 +1,6 @@
<script>
import { GlIntersectionObserver, GlSafeHtmlDirective } from '@gitlab/ui';
+import { scrollToElement } from '~/lib/utils/common_utils';
import ChunkLine from './chunk_line.vue';
/*
@@ -23,6 +24,11 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
props: {
+ isFirstChunk: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
chunkIndex: {
type: Number,
required: false,
@@ -46,6 +52,11 @@ export default {
required: false,
default: 0,
},
+ totalChunks: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
language: {
type: String,
required: false,
@@ -56,53 +67,68 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
computed: {
lines() {
return this.content.split('\n');
},
},
+
+ created() {
+ if (this.isFirstChunk) {
+ this.isLoading = false;
+ return;
+ }
+
+ window.requestIdleCallback(() => {
+ this.isLoading = false;
+ const { hash } = this.$route;
+ if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) {
+ // when the last chunk is loaded scroll to the hash
+ scrollToElement(hash, { behavior: 'auto' });
+ }
+ });
+ },
methods: {
handleChunkAppear() {
if (!this.isHighlighted) {
this.$emit('appear', this.chunkIndex);
}
},
+ calculateLineNumber(index) {
+ return this.startingFrom + index + 1;
+ },
},
};
</script>
<template>
- <div>
- <gl-intersection-observer @appear="handleChunkAppear">
- <div v-if="isHighlighted">
- <chunk-line
- v-for="(line, index) in lines"
+ <gl-intersection-observer @appear="handleChunkAppear">
+ <div v-if="isHighlighted">
+ <chunk-line
+ v-for="(line, index) in lines"
+ :key="index"
+ :number="calculateLineNumber(index)"
+ :content="line"
+ :language="language"
+ :blame-path="blamePath"
+ />
+ </div>
+ <div v-else-if="!isLoading" class="gl-display-flex gl-text-transparent">
+ <div class="gl-display-flex gl-flex-direction-column content-visibility-auto">
+ <span
+ v-for="(n, index) in totalLines"
+ v-once
+ :id="`L${calculateLineNumber(index)}`"
:key="index"
- :number="startingFrom + index + 1"
- :content="line"
- :language="language"
- :blame-path="blamePath"
- />
- </div>
- <div v-else class="gl-display-flex">
- <div class="gl-display-flex gl-flex-direction-column">
- <a
- v-for="(n, index) in totalLines"
- :id="`L${startingFrom + index + 1}`"
- :key="index"
- class="gl-ml-5 gl-text-transparent"
- :href="`#L${startingFrom + index + 1}`"
- :data-line-number="startingFrom + index + 1"
- data-testid="line-number"
- >
- {{ startingFrom + index + 1 }}
- </a>
- </div>
- <div
- class="gl-white-space-pre-wrap! gl-text-transparent"
- data-testid="content"
- v-text="content"
- ></div>
+ data-testid="line-number"
+ v-text="calculateLineNumber(index)"
+ ></span>
</div>
- </gl-intersection-observer>
- </div>
+ <div v-once class="gl-white-space-pre-wrap!" data-testid="content">{{ content }}</div>
+ </div>
+ </gl-intersection-observer>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index ffd0eea63a1..0bf19f83d86 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,6 +1,7 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getPageParamValue, getPageSearchString } from '~/blob/utils';
export default {
directives: {
@@ -25,6 +26,13 @@ export default {
required: true,
},
},
+ computed: {
+ pageSearchString() {
+ if (!this.glFeatures.fileLineBlame) return '';
+ const page = getPageParamValue(this.number);
+ return getPageSearchString(this.blamePath, page);
+ },
+ },
};
</script>
<template>
@@ -35,7 +43,7 @@ export default {
<a
v-if="glFeatures.fileLineBlame"
class="gl-user-select-none gl-shadow-none! file-line-blame"
- :href="`${blamePath}#L${number}`"
+ :href="`${blamePath}${pageSearchString}#L${number}`"
></a>
<a
:id="`L${number}`"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
index d957990fe7f..fca2616f069 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
@@ -1,9 +1,17 @@
import packageJsonLinker from './utils/package_json_linker';
import gemspecLinker from './utils/gemspec_linker';
+import godepsJsonLinker from './utils/godeps_json_linker';
+import gemfileLinker from './utils/gemfile_linker';
+import podspecJsonLinker from './utils/podspec_json_linker';
+import composerJsonLinker from './utils/composer_json_linker';
const DEPENDENCY_LINKERS = {
package_json: packageJsonLinker,
gemspec: gemspecLinker,
+ godeps_json: godepsJsonLinker,
+ gemfile: gemfileLinker,
+ podspec_json: podspecJsonLinker,
+ composer_json: composerJsonLinker,
};
/**
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js
new file mode 100644
index 00000000000..f5c4c886546
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/composer_json_linker.js
@@ -0,0 +1,49 @@
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const PACKAGIST_URL = 'https://packagist.org/packages/';
+const DRUPAL_URL = 'https://www.drupal.org/project/';
+
+const attrOpenTag = generateHLJSOpenTag('attr');
+const stringOpenTag = generateHLJSOpenTag('string');
+const closeTag = '&quot;</span>';
+const DRUPAL_PROJECT_SEPARATOR = 'drupal/';
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;composer/installers&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;^1.2&quot;</span>
+ * Group 1: composer/installers
+ * Group 2: ^1.2
+ */
+ `${attrOpenTag}([^/]+/[^/]+.)${closeTag}.*${stringOpenTag}(.*[0-9].*)(${closeTag})`,
+ 'gm',
+);
+
+const handleReplace = (original, packageName, version, dependenciesToLink) => {
+ const isDrupalDependency = packageName.includes(DRUPAL_PROJECT_SEPARATOR);
+ const href = isDrupalDependency
+ ? `${DRUPAL_URL}${packageName.split(DRUPAL_PROJECT_SEPARATOR)[1]}`
+ : `${PACKAGIST_URL}${packageName}`;
+ const packageLink = createLink(href, packageName);
+ const versionLink = createLink(href, version);
+ const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
+ const dependencyToLink = dependenciesToLink[packageName];
+
+ if (dependencyToLink && dependencyToLink === version) {
+ return `${attrOpenTag}${packageLink}${closeAndOpenTag}${versionLink}${closeTag}`;
+ }
+
+ return original;
+};
+
+export default (result, raw) => {
+ const rawParsed = JSON.parse(raw);
+
+ const dependenciesToLink = {
+ ...rawParsed.require,
+ ...rawParsed['require-dev'],
+ };
+
+ return result.value.replace(DEPENDENCY_REGEX, (original, packageName, version) =>
+ handleReplace(original, packageName, version, dependenciesToLink),
+ );
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
index 49704421d6e..c1a1101afad 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -1,7 +1,27 @@
import { escape } from 'lodash';
export const createLink = (href, innerText) =>
- `<a href="${escape(href)}" rel="nofollow noreferrer noopener">${escape(innerText)}</a>`;
+ `<a href="${escape(href)}" target="_blank" rel="nofollow noreferrer noopener">${escape(
+ innerText,
+ )}</a>`;
export const generateHLJSOpenTag = (type, delimiter = '&quot;') =>
`<span class="hljs-${escape(type)}">${delimiter}`;
+
+export const getObjectKeysByKeyName = (obj, keyName, acc) => {
+ if (obj instanceof Array) {
+ obj.map((subObj) => getObjectKeysByKeyName(subObj, keyName, acc));
+ } else {
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ if (key === keyName) {
+ acc.push(...Object.keys(obj[key]));
+ }
+ if (obj[key] instanceof Object || obj[key] instanceof Array) {
+ getObjectKeysByKeyName(obj[key], keyName, acc);
+ }
+ }
+ }
+ }
+ return acc;
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js
new file mode 100644
index 00000000000..81389763f49
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemfile_linker.js
@@ -0,0 +1,25 @@
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const GEM_URL = 'https://rubygems.org/gems/';
+const GEM_STRING = 'gem </span>';
+const delimiter = '&#39;';
+const stringOpenTag = generateHLJSOpenTag('string', delimiter);
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: 'gem </span><span class="hljs-string">&#39;paranoia&#39;'
+ * Group 1 (packageName) : 'paranoia'
+ */
+ `${GEM_STRING}${stringOpenTag}(.+?(?=${delimiter}))`,
+ 'gm',
+);
+
+const handleReplace = (packageName) => {
+ const href = `${GEM_URL}${packageName}`;
+ const packageLink = createLink(href, packageName);
+ return `${GEM_STRING}${stringOpenTag}${packageLink}`;
+};
+export default (result) => {
+ return result.value.replace(DEPENDENCY_REGEX, (_, packageName) => handleReplace(packageName));
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js
new file mode 100644
index 00000000000..bff8e3cf410
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/godeps_json_linker.js
@@ -0,0 +1,64 @@
+import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+
+const PROTOCOL = 'https://';
+const GODOCS_DOMAIN = 'godoc.org/';
+const REPO_PATH = '/tree/master/';
+const GODOCS_REGEX = /golang.org/;
+const GITLAB_REPO_PATH = `/_${REPO_PATH}`;
+const REPO_REGEX = `[^/'"]+/[^/'"]+`;
+const NESTED_REPO_REGEX = '([^/]+/)+[^/]+?';
+const GITHUB_REPO_REGEX = new RegExp(`(github.com/${REPO_REGEX})/(.+)`);
+const GITLAB_REPO_REGEX = new RegExp(`(gitlab.com/${REPO_REGEX})/(.+)`);
+const GITLAB_NESTED_REPO_REGEX = new RegExp(`(gitlab.com/${NESTED_REPO_REGEX}).git/(.+)`);
+const attrOpenTag = generateHLJSOpenTag('attr');
+const stringOpenTag = generateHLJSOpenTag('string');
+const closeTag = '&quot;</span>';
+const importPathString =
+ 'ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span>';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;ImportPath&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-string">&quot;github.com/ayufan/golang-kardianos-service&quot;</span>
+ * Group 1: github.com/ayufan/golang-kardianos-service
+ */
+ `${importPathString}${stringOpenTag}(.*)${closeTag}`,
+ 'gm',
+);
+
+const replaceRepoPath = (dependency, regex, repoPath) =>
+ dependency.replace(regex, (_, repo, path) => `${PROTOCOL}${repo}${repoPath}${path}`);
+
+const regexConfigs = [
+ {
+ matcher: GITHUB_REPO_REGEX,
+ resolver: (dep) => replaceRepoPath(dep, GITHUB_REPO_REGEX, REPO_PATH),
+ },
+ {
+ matcher: GITLAB_REPO_REGEX,
+ resolver: (dep) => replaceRepoPath(dep, GITLAB_REPO_REGEX, GITLAB_REPO_PATH),
+ },
+ {
+ matcher: GITLAB_NESTED_REPO_REGEX,
+ resolver: (dep) => replaceRepoPath(dep, GITLAB_NESTED_REPO_REGEX, GITLAB_REPO_PATH),
+ },
+ {
+ matcher: GODOCS_REGEX,
+ resolver: (dep) => `${PROTOCOL}${GODOCS_DOMAIN}${dep}`,
+ },
+];
+
+const getLinkHref = (dependency) => {
+ const regexConfig = regexConfigs.find((config) => dependency.match(config.matcher));
+ return regexConfig ? regexConfig.resolver(dependency) : `${PROTOCOL}${dependency}`;
+};
+
+const handleReplace = (dependency) => {
+ const linkHref = getLinkHref(dependency);
+ const link = createLink(linkHref, dependency);
+ return `${importPathString}${attrOpenTag}${link}${closeTag}`;
+};
+
+export default (result) => {
+ return result.value.replace(DEPENDENCY_REGEX, (_, dependency) => handleReplace(dependency));
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js
new file mode 100644
index 00000000000..e2007fe408b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/podspec_json_linker.js
@@ -0,0 +1,32 @@
+import { createLink, generateHLJSOpenTag, getObjectKeysByKeyName } from './dependency_linker_util';
+
+const COCOAPODS_URL = 'https://cocoapods.org/pods/';
+const beginString = generateHLJSOpenTag('attr');
+const endString =
+ '&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation">\\[';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: <span class="hljs-attr">&quot;AFNetworking/Security&quot;</span><span class="hljs-punctuation">:</span><span class=""> </span><span class="hljs-punctuation"> [
+ * Group 1: AFNetworking/Serialization
+ */
+ `${beginString}([^/]+/?[^/]+.)${endString}`,
+ 'gm',
+);
+
+const handleReplace = (original, dependency, dependenciesToLink) => {
+ if (dependenciesToLink.includes(dependency)) {
+ const href = `${COCOAPODS_URL}${dependency.split('/')[0]}`;
+ const link = createLink(href, dependency);
+ return `${beginString}${link}${endString.replace('\\', '')}`;
+ }
+ return original;
+};
+
+export default (result, raw) => {
+ const dependenciesToLink = getObjectKeysByKeyName(JSON.parse(raw), 'dependencies', []);
+ return result.value.replace(DEPENDENCY_REGEX, (original, dependency) =>
+ handleReplace(original, dependency, dependenciesToLink),
+ );
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
index e0ba4b730a7..3540ac6caf1 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
@@ -22,7 +22,7 @@ const format = (node, kind = '') => {
.split(newlineRegex)
.map((newline) => generateHLJSTag(kind, newline, true))
.join('\n');
- } else if (node.kind) {
+ } else if (node.kind || node.sublanguage) {
const { children } = node;
if (children.length && children.length === 1) {
buffer += format(children[0], node.kind);
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 536b2c8a281..f621a23734a 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
@@ -65,6 +65,9 @@ export default {
!supportedLanguages.includes(this.blob.language?.toLowerCase())
);
},
+ totalChunks() {
+ return Object.keys(this.chunks).length;
+ },
},
async created() {
addBlobLinksTracking();
@@ -200,6 +203,7 @@ export default {
:content="firstChunk.content"
:starting-from="firstChunk.startingFrom"
:is-highlighted="firstChunk.isHighlighted"
+ is-first-chunk
:language="firstChunk.language"
:blame-path="blob.blamePath"
/>
@@ -217,6 +221,7 @@ export default {
:chunk-index="index"
:language="chunk.language"
:blame-path="blob.blamePath"
+ :total-chunks="totalChunks"
@appear="highlightChunk"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
index e621442e601..84615386fe2 100644
--- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
+++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.stories.js
@@ -1,4 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
import TooltipOnTruncate from './tooltip_on_truncate.vue';
const defaultWidth = '250px';
diff --git a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
index 1f0f4cde234..0815fdd9aac 100644
--- a/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
+++ b/app/assets/javascripts/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.stories.js
@@ -1,5 +1,3 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-
import { OBSTACLE_TYPES } from './constants';
import UserDeletionObstaclesList from './user_deletion_obstacles_list.vue';
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 7e735f358eb..30b7b073ac3 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
@@ -3,7 +3,7 @@ import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlSprintf, GlTooltipDirective
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import { differenceInSeconds, getTimeago, SECONDS_IN_DAY } from '~/lib/utils/datetime_utility';
+import { getTimeago } from '~/lib/utils/datetime_utility';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
@@ -62,9 +62,8 @@ export default {
issuableId() {
return getIdFromGraphQLId(this.issuable.id);
},
- createdInPastDay() {
- const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date());
- return createdSecondsAgo < SECONDS_IN_DAY;
+ issuableIid() {
+ return this.issuable.iid;
},
author() {
return this.issuable.author || {};
@@ -184,7 +183,7 @@ export default {
<li
:id="`issuable_${issuableId}`"
class="issue gl-display-flex! gl-px-5!"
- :class="{ closed: issuable.closedAt, today: createdInPastDay }"
+ :class="{ closed: issuable.closedAt }"
:data-labels="labelIdsString"
:data-qa-issue-id="issuableId"
>
@@ -193,6 +192,8 @@ export default {
class="issue-check gl-mr-0"
:checked="checked"
:data-id="issuableId"
+ :data-iid="issuableIid"
+ :data-type="issuable.type"
@input="$emit('checked-input', $event)"
>
<span class="gl-sr-only">{{ issuable.title }}</span>
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 bc10f84b819..dd3d7c8f4d6 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
@@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import issuableEventHub from '~/issues/list/eventhub';
import { DEFAULT_SKELETON_COUNT, PAGE_SIZE_STORAGE_KEY } from '../constants';
import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue';
import IssuableItem from './issuable_item.vue';
@@ -177,6 +178,11 @@ export default {
required: false,
default: false,
},
+ showFilteredSearchFriendlyText: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
showPageSizeChangeControls: {
type: Boolean,
required: false,
@@ -266,6 +272,7 @@ export default {
handleIssuableCheckedInput(issuable, value) {
this.checkedIssuables[this.issuableId(issuable)].checked = value;
this.$emit('update-legacy-bulk-edit');
+ issuableEventHub.$emit('issuables:issuableChecked', issuable, value);
},
handleAllIssuablesCheckedInput(value) {
Object.keys(this.checkedIssuables).forEach((issuableId) => {
@@ -308,6 +315,7 @@ export default {
:sync-filter-and-sort="syncFilterAndSort"
:show-checkbox="showBulkEditSidebar"
:checkbox-checked="allIssuablesChecked"
+ :show-friendly-text="showFilteredSearchFriendlyText"
class="gl-flex-grow-1 gl-border-t-none row-content-block"
data-qa-selector="issuable_search_container"
@checked-input="handleAllIssuablesCheckedInput"
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index 6a4f671abb9..a6628fa0f9f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -90,7 +90,7 @@ const createStatusMessage = ({ reportType, status, total }) => {
if (status) {
message = __('%{reportType} %{status}');
} else if (!total) {
- message = __('%{reportType} detected no %{totalStart}new%{totalEnd} vulnerabilities.');
+ message = __('%{reportType} detected no new vulnerabilities.');
} else {
message = __(
'%{reportType} detected %{totalStart}%{total}%{totalEnd} potential %{vulnMessage}',
diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue
index 5ec16d4ba15..4fafeff8804 100644
--- a/app/assets/javascripts/webhooks/components/form_url_app.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_app.vue
@@ -1,7 +1,8 @@
<script>
-import { isEmpty } from 'lodash';
+import { cloneDeep, isEmpty } from 'lodash';
import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import { scrollToElement } from '~/lib/utils/common_utils';
import FormUrlMaskItem from './form_url_mask_item.vue';
@@ -30,10 +31,15 @@ export default {
return {
maskEnabled: !isEmpty(this.initialUrlVariables),
url: this.initialUrl,
- items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
+ items: this.getInitialItems(),
+ isValidated: false,
+ formEl: null,
};
},
computed: {
+ urlState() {
+ return !this.isValidated || !isEmpty(this.url);
+ },
maskedUrl() {
if (!this.url) {
return null;
@@ -46,14 +52,83 @@ export default {
return;
}
- const replacementExpression = new RegExp(value, 'g');
- maskedUrl = maskedUrl.replace(replacementExpression, `{${key}}`);
+ maskedUrl = this.maskUrl(maskedUrl, key, value);
});
return maskedUrl;
},
},
+ mounted() {
+ this.formEl = document.querySelector('.js-webhook-form');
+
+ this.formEl?.addEventListener('submit', this.handleSubmit);
+ },
+ destroy() {
+ this.formEl?.removeEventListener('submit', this.handleSubmit);
+ },
methods: {
+ getInitialItems() {
+ return isEmpty(this.initialUrlVariables) ? [{}] : cloneDeep(this.initialUrlVariables);
+ },
+ isEditingItem(index, key) {
+ if (isEmpty(this.initialUrlVariables)) {
+ return false;
+ }
+
+ const item = this.initialUrlVariables[index];
+ return item && item.key === key;
+ },
+ keyInvalidFeedback(key) {
+ if (this.isValidated && isEmpty(key)) {
+ return this.$options.i18n.inputRequired;
+ }
+
+ return null;
+ },
+ valueInvalidFeedback(index, key, value) {
+ if (this.isEditingItem(index, key)) {
+ return null;
+ }
+
+ if (this.isValidated && isEmpty(value)) {
+ return this.$options.i18n.inputRequired;
+ }
+
+ if (!isEmpty(value) && !this.url?.includes(value)) {
+ return this.$options.i18n.valuePartOfUrl;
+ }
+
+ return null;
+ },
+ isValid() {
+ this.isValidated = true;
+
+ if (!this.urlState) {
+ return false;
+ }
+
+ if (
+ this.maskEnabled &&
+ this.items.some(
+ ({ key, value }, index) =>
+ this.keyInvalidFeedback(key) || this.valueInvalidFeedback(index, key, value),
+ )
+ ) {
+ return false;
+ }
+
+ return true;
+ },
+ handleSubmit(e) {
+ if (!this.isValid()) {
+ scrollToElement(this.$refs.formUrl.$el);
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ },
+ maskUrl(url, key, value) {
+ return url.split(value).join(`{${key}}`);
+ },
onItemInput({ index, key, value }) {
this.$set(this.items, index, { key, value });
},
@@ -66,6 +141,7 @@ export default {
},
i18n: {
addItem: s__('Webhooks|+ Mask another portion of URL'),
+ inputRequired: __('This field is required.'),
radioFullUrlText: s__('Webhooks|Show full URL'),
radioMaskUrlText: s__('Webhooks|Mask portions of URL'),
radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'),
@@ -75,6 +151,7 @@ export default {
urlLabel: __('URL'),
urlPlaceholder: 'http://example.com/trigger-ci.json',
urlPreview: s__('Webhooks|URL preview'),
+ valuePartOfUrl: s__('Webhooks|Must match part of URL'),
},
};
</script>
@@ -82,14 +159,18 @@ export default {
<template>
<div>
<gl-form-group
+ ref="formUrl"
:label="$options.i18n.urlLabel"
label-for="webhook-url"
:description="$options.i18n.urlDescription"
+ :invalid-feedback="$options.i18n.inputRequired"
+ :state="urlState"
>
<gl-form-input
id="webhook-url"
v-model="url"
name="hook[url]"
+ :state="urlState"
:placeholder="$options.i18n.urlPlaceholder"
data-testid="form-url"
/>
@@ -112,6 +193,9 @@ export default {
:index="index"
:item-key="key"
:item-value="value"
+ :is-editing="isEditingItem(index, key)"
+ :key-invalid-feedback="keyInvalidFeedback(key)"
+ :value-invalid-feedback="valueInvalidFeedback(index, key, value)"
@input="onItemInput"
@remove="removeItem"
/>
diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
index 3b75f9b6c0d..f5f81759719 100644
--- a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
@@ -1,6 +1,8 @@
<script>
+import { isEmpty } from 'lodash';
import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { MASK_ITEM_VALUE_HIDDEN } from '../constants';
export default {
components: {
@@ -24,6 +26,21 @@ export default {
required: false,
default: null,
},
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ keyInvalidFeedback: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ valueInvalidFeedback: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
keyInputId() {
@@ -32,6 +49,15 @@ export default {
valueInputId() {
return this.inputId('value');
},
+ keyState() {
+ return isEmpty(this.keyInvalidFeedback);
+ },
+ valueState() {
+ return isEmpty(this.valueInvalidFeedback);
+ },
+ displayValue() {
+ return this.isEditing ? MASK_ITEM_VALUE_HIDDEN : this.itemValue;
+ },
},
methods: {
inputId(type) {
@@ -58,23 +84,29 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-3">
+ <div class="gl-display-flex gl-align-items-flex-start gl-gap-3 gl-mb-3">
<gl-form-group
:label="$options.i18n.valueLabel"
:label-for="valueInputId"
+ :invalid-feedback="valueInvalidFeedback"
+ :state="valueState"
class="gl-flex-grow-1 gl-mb-0"
data-testid="mask-item-value"
>
<gl-form-input
:id="valueInputId"
:name="inputName('value')"
- :value="itemValue"
+ :value="displayValue"
+ :disabled="isEditing"
+ :state="valueState"
@input="onValueInput"
/>
</gl-form-group>
<gl-form-group
:label="$options.i18n.keyLabel"
:label-for="keyInputId"
+ :invalid-feedback="keyInvalidFeedback"
+ :state="keyState"
class="gl-flex-grow-1 gl-mb-0"
data-testid="mask-item-key"
>
@@ -82,9 +114,17 @@ export default {
:id="keyInputId"
:name="inputName('key')"
:value="itemKey"
+ :disabled="isEditing"
+ :state="keyState"
@input="onKeyInput"
/>
</gl-form-group>
- <gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
+ <gl-button
+ icon="remove"
+ :aria-label="__('Remove')"
+ :disabled="isEditing"
+ class="gl-mt-6"
+ @click="onRemoveClick"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue
new file mode 100644
index 00000000000..677f06314e0
--- /dev/null
+++ b/app/assets/javascripts/webhooks/components/push_events.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlFormCheckbox, GlFormRadio, GlFormRadioGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
+import {
+ BRANCH_FILTER_ALL_BRANCHES,
+ WILDCARD_CODE_STABLE,
+ WILDCARD_CODE_PRODUCTION,
+ REGEX_CODE,
+ descriptionText,
+} from '~/webhooks/constants';
+
+export default {
+ components: {
+ GlFormCheckbox,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlFormInput,
+ GlSprintf,
+ },
+ inject: ['pushEvents', 'strategy', 'isNewHook', 'pushEventsBranchFilter'],
+ data() {
+ return {
+ pushEventsData: !this.isNewHook && this.pushEvents,
+ branchFilterStrategyData: this.isNewHook ? BRANCH_FILTER_ALL_BRANCHES : this.strategy,
+ pushEventsBranchFilterData: this.pushEventsBranchFilter,
+ };
+ },
+ WILDCARD_CODE_STABLE,
+ WILDCARD_CODE_PRODUCTION,
+ REGEX_CODE,
+ descriptionText,
+};
+</script>
+
+<template>
+ <div>
+ <gl-form-checkbox v-model="pushEventsData">{{ s__('Webhooks|Push events') }}</gl-form-checkbox>
+ <input type="hidden" :value="pushEventsData" name="hook[push_events]" />
+
+ <div v-if="pushEventsData" class="gl-pl-6">
+ <gl-form-radio-group v-model="branchFilterStrategyData" name="hook[branch_filter_strategy]">
+ <gl-form-radio
+ class="gl-mt-2 branch-filter-strategy-radio"
+ value="all_branches"
+ data-testid="rule_all_branches"
+ >
+ <div data-qa-selector="strategy_radio_all">{{ __('All branches') }}</div>
+ </gl-form-radio>
+
+ <!-- wildcard -->
+ <gl-form-radio
+ class="gl-mt-2 branch-filter-strategy-radio"
+ value="wildcard"
+ data-testid="rule_wildcard"
+ >
+ <div data-qa-selector="strategy_radio_wildcard">
+ {{ s__('Webhooks|Wildcard pattern') }}
+ </div>
+ </gl-form-radio>
+ <div class="gl-ml-6">
+ <gl-form-input
+ v-if="branchFilterStrategyData === 'wildcard'"
+ v-model="pushEventsBranchFilterData"
+ name="hook[push_events_branch_filter]"
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ />
+ </div>
+ <p
+ v-if="branchFilterStrategyData === 'wildcard'"
+ class="form-text text-muted custom-control"
+ >
+ <gl-sprintf :message="$options.descriptionText.wildcard">
+ <template #WILDCARD_CODE_STABLE>
+ <code>{{ $options.WILDCARD_CODE_STABLE }}</code>
+ </template>
+ <template #WILDCARD_CODE_PRODUCTION>
+ <code>{{ $options.WILDCARD_CODE_PRODUCTION }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <!-- regex -->
+ <gl-form-radio
+ class="gl-mt-2 branch-filter-strategy-radio"
+ value="regex"
+ data-testid="rule_regex"
+ >
+ <div data-qa-selector="strategy_radio_regex">
+ {{ s__('Webhooks|Regular expression') }}
+ </div>
+ </gl-form-radio>
+ <div class="gl-ml-6">
+ <gl-form-input
+ v-if="branchFilterStrategyData === 'regex'"
+ v-model="pushEventsBranchFilterData"
+ name="hook[push_events_branch_filter]"
+ data-qa-selector="webhook_branch_filter_field"
+ data-testid="webhook_branch_filter_field"
+ />
+ </div>
+
+ <p v-if="branchFilterStrategyData === 'regex'" class="form-text text-muted custom-control">
+ <gl-sprintf :message="$options.descriptionText.regex">
+ <template #REGEX_CODE>
+ <code>{{ $options.REGEX_CODE }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-form-radio-group>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/webhooks/constants.js b/app/assets/javascripts/webhooks/constants.js
new file mode 100644
index 00000000000..6710a418117
--- /dev/null
+++ b/app/assets/javascripts/webhooks/constants.js
@@ -0,0 +1,19 @@
+import { s__ } from '~/locale';
+
+export const BRANCH_FILTER_ALL_BRANCHES = 'all_branches';
+export const BRANCH_FILTER_WILDCARD = 'wildcard';
+export const BRANCH_FILTER_REGEX = 'regex';
+
+export const WILDCARD_CODE_STABLE = '*-stable';
+export const WILDCARD_CODE_PRODUCTION = 'production/*';
+
+export const REGEX_CODE = '(feature|hotfix)/*';
+
+export const descriptionText = {
+ [BRANCH_FILTER_WILDCARD]: s__(
+ 'Webhooks|Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported.',
+ ),
+ [BRANCH_FILTER_REGEX]: s__('Webhooks|Regex such as %{REGEX_CODE} is supported.'),
+};
+
+export const MASK_ITEM_VALUE_HIDDEN = '************';
diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js
index 1b2b33e44c1..7d04978280b 100644
--- a/app/assets/javascripts/webhooks/index.js
+++ b/app/assets/javascripts/webhooks/index.js
@@ -17,7 +17,7 @@ export default () => {
return createElement(FormUrlApp, {
props: {
initialUrl,
- initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
+ initialUrlVariables: JSON.parse(urlVariables),
},
});
},
diff --git a/app/assets/javascripts/webhooks/webhook.js b/app/assets/javascripts/webhooks/webhook.js
new file mode 100644
index 00000000000..ca631502745
--- /dev/null
+++ b/app/assets/javascripts/webhooks/webhook.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import pushEvents from './components/push_events.vue';
+
+export function initPushEventsEditForm() {
+ const el = document.querySelector('.js-vue-push-events');
+
+ if (!el) return false;
+
+ const provide = {
+ isNewHook: parseBoolean(el.dataset.isNewHook),
+ pushEvents: parseBoolean(el.dataset.pushEvents),
+ strategy: el.dataset.strategy,
+ pushEventsBranchFilter: el.dataset.pushEventsBranchFilter,
+ };
+ return new Vue({
+ el,
+ provide,
+ render(createElement) {
+ return createElement(pushEvents);
+ },
+ });
+}
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 57babe4569d..57930951856 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlFormGroup, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlFormGroup } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
@@ -7,22 +7,25 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import { __, s__ } from '~/locale';
import EditedAt from '~/issues/show/components/edited.vue';
import Tracking from '~/tracking';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import workItemQuery from '../graphql/work_item.query.graphql';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { getWorkItemQuery } from '../utils';
+import workItemDescriptionSubscription from '../graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants';
+import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
export default {
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
components: {
EditedAt,
GlButton,
GlFormGroup,
+ MarkdownEditor,
MarkdownField,
+ WorkItemDescriptionRendered,
},
- mixins: [Tracking.mixin()],
+ mixins: [glFeatureFlagMixin(), Tracking.mixin()],
props: {
workItemId: {
type: String,
@@ -32,6 +35,15 @@ export default {
type: String,
required: true,
},
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
},
markdownDocsPath: helpPagePath('user/markdown'),
data() {
@@ -41,21 +53,37 @@ export default {
isSubmitting: false,
isSubmittingWithKeydown: false,
descriptionText: '',
+ descriptionHtml: '',
};
},
apollo: {
workItem: {
- query: workItemQuery,
+ query() {
+ return getWorkItemQuery(this.fetchByIid);
+ },
variables() {
- return {
- id: this.workItemId,
- };
+ return this.queryVariables;
+ },
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
},
skip() {
return !this.workItemId;
},
+ result() {
+ this.descriptionText = this.workItemDescription?.description;
+ this.descriptionHtml = this.workItemDescription?.descriptionHtml;
+ },
error() {
- this.error = i18n.fetchError;
+ this.$emit('error', i18n.fetchError);
+ },
+ subscribeToMore: {
+ document: workItemDescriptionSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
},
},
},
@@ -64,7 +92,7 @@ export default {
return this.workItemId;
},
canEdit() {
- return this.workItem?.userPermissions?.updateWorkItem;
+ return this.workItem?.userPermissions?.updateWorkItem || false;
},
tracking() {
return {
@@ -73,12 +101,6 @@ export default {
property: `type_${this.workItemType}`,
};
},
- descriptionHtml() {
- return this.workItemDescription?.descriptionHtml;
- },
- descriptionEmpty() {
- return this.descriptionHtml?.trim() === '';
- },
workItemDescription() {
const descriptionWidget = this.workItem?.widgets?.find(
(widget) => widget.type === WIDGET_TYPE_DESCRIPTION,
@@ -114,7 +136,7 @@ export default {
await this.$nextTick();
- this.$refs.textarea.focus();
+ this.$refs.textarea?.focus();
},
async cancelEditing() {
const isDirty = this.descriptionText !== this.workItemDescription?.description;
@@ -142,8 +164,10 @@ export default {
updateDraft(this.autosaveKey, this.descriptionText);
},
- async updateWorkItem(event) {
- if (event.key) {
+ async updateWorkItem(event = {}) {
+ const { key } = event;
+
+ if (key) {
this.isSubmittingWithKeydown = true;
}
@@ -179,73 +203,90 @@ export default {
this.isSubmitting = false;
},
+ setDescriptionText(newText) {
+ this.descriptionText = newText;
+ updateDraft(this.autosaveKey, this.descriptionText);
+ },
+ handleDescriptionTextUpdated(newText) {
+ this.descriptionText = newText;
+ this.updateWorkItem();
+ },
},
};
</script>
<template>
- <gl-form-group
- v-if="isEditing"
- class="gl-my-5 gl-border-t gl-pt-6"
- :label="__('Description')"
- label-for="work-item-description"
- >
- <markdown-field
- can-attach-file
- :textarea-value="descriptionText"
- :is-submitting="isSubmitting"
- :markdown-preview-path="markdownPreviewPath"
- :markdown-docs-path="$options.markdownDocsPath"
- class="gl-p-3 bordered-box gl-mt-5"
+ <div>
+ <gl-form-group
+ v-if="isEditing"
+ class="gl-mb-5 gl-border-t gl-pt-6"
+ :label="__('Description')"
+ label-for="work-item-description"
>
- <template #textarea>
- <textarea
- id="work-item-description"
- ref="textarea"
- v-model="descriptionText"
- :disabled="isSubmitting"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- dir="auto"
- data-supports-quick-actions="false"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment or drag your files here…')"
- @keydown.meta.enter="updateWorkItem"
- @keydown.ctrl.enter="updateWorkItem"
- @keydown.exact.esc.stop="cancelEditing"
- @input="onInput"
- ></textarea>
- </template>
- </markdown-field>
-
- <div class="gl-display-flex">
- <gl-button
- category="primary"
- variant="confirm"
- :loading="isSubmitting"
- data-testid="save-description"
- @click="updateWorkItem"
- >{{ __('Save') }}</gl-button
- >
- <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing">{{
- __('Cancel')
- }}</gl-button>
- </div>
- </gl-form-group>
- <div v-else class="gl-mb-5 gl-border-t">
- <div class="gl-display-inline-flex gl-align-items-center gl-mb-5">
- <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
- <gl-button
- v-if="canEdit"
- class="gl-ml-auto"
- icon="pencil"
- data-testid="edit-description"
- :aria-label="__('Edit description')"
- @click="startEditing"
+ <markdown-editor
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-my-3 common-note-form"
+ :value="descriptionText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.markdownDocsPath"
+ :form-field-aria-label="__('Description')"
+ :form-field-placeholder="__('Write a comment or drag your files here…')"
+ form-field-id="work-item-description"
+ form-field-name="work-item-description"
+ enable-autocomplete
+ init-on-autofocus
+ @input="setDescriptionText"
+ @keydown.meta.enter="updateWorkItem"
+ @keydown.ctrl.enter="updateWorkItem"
/>
- </div>
-
- <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
- <div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
+ <markdown-field
+ v-else
+ can-attach-file
+ :textarea-value="descriptionText"
+ :is-submitting="isSubmitting"
+ :markdown-preview-path="markdownPreviewPath"
+ :markdown-docs-path="$options.markdownDocsPath"
+ class="gl-p-3 bordered-box gl-mt-5"
+ >
+ <template #textarea>
+ <textarea
+ id="work-item-description"
+ ref="textarea"
+ v-model="descriptionText"
+ :disabled="isSubmitting"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ @keydown.meta.enter="updateWorkItem"
+ @keydown.ctrl.enter="updateWorkItem"
+ @keydown.exact.esc.stop="cancelEditing"
+ @input="onInput"
+ ></textarea>
+ </template>
+ </markdown-field>
+ <div class="gl-display-flex">
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ __('Save') }}
+ </gl-button>
+ <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing"
+ >{{ __('Cancel') }}
+ </gl-button>
+ </div>
+ </gl-form-group>
+ <work-item-description-rendered
+ v-else
+ :work-item-description="workItemDescription"
+ :can-edit="canEdit"
+ @startEditing="startEditing"
+ @descriptionUpdated="handleDescriptionTextUpdated"
+ />
<edited-at
v-if="lastEditedAt"
:updated-at="lastEditedAt"
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
new file mode 100644
index 00000000000..e6f8a301c5e
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -0,0 +1,117 @@
+<script>
+import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import $ from 'jquery';
+import '~/behaviors/markdown/render_gfm';
+
+const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
+
+export default {
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ components: {
+ GlButton,
+ },
+ props: {
+ workItemDescription: {
+ type: Object,
+ required: true,
+ },
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ descriptionText() {
+ return this.workItemDescription?.description;
+ },
+ descriptionHtml() {
+ return this.workItemDescription?.descriptionHtml;
+ },
+ descriptionEmpty() {
+ return this.descriptionHtml?.trim() === '';
+ },
+ },
+ watch: {
+ descriptionHtml: {
+ handler() {
+ this.renderGFM();
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ async renderGFM() {
+ await this.$nextTick();
+
+ $(this.$refs['gfm-content']).renderGFM();
+
+ if (this.canEdit) {
+ this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
+
+ // enable boxes, disabled by default in markdown
+ this.checkboxes.forEach((checkbox) => {
+ // eslint-disable-next-line no-param-reassign
+ checkbox.disabled = false;
+ });
+ }
+ },
+ toggleCheckboxes(event) {
+ const { target } = event;
+
+ if (isCheckbox(target)) {
+ target.disabled = true;
+
+ const { sourcepos } = target.parentElement.dataset;
+
+ if (!sourcepos) return;
+
+ const [startRange] = sourcepos.split('-');
+ let [startRow] = startRange.split(':');
+ startRow = Number(startRow) - 1;
+
+ const descriptionTextRows = this.descriptionText.split('\n');
+ const newDescriptionText = descriptionTextRows
+ .map((row, index) => {
+ if (startRow === index) {
+ if (target.checked) {
+ return row.replace(/\[ \]/, '[x]');
+ }
+ return row.replace(/\[[x~]\]/i, '[ ]');
+ }
+ return row;
+ })
+ .join('\n');
+
+ this.$emit('descriptionUpdated', newDescriptionText);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-mb-5 gl-border-t gl-pt-5">
+ <div class="gl-display-inline-flex gl-align-items-center gl-mb-5">
+ <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
+ <gl-button
+ v-if="canEdit"
+ class="gl-ml-auto"
+ icon="pencil"
+ data-testid="edit-description"
+ :aria-label="__('Edit description')"
+ @click="$emit('startEditing')"
+ />
+ </div>
+
+ <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
+ <div
+ v-else
+ ref="gfm-content"
+ v-safe-html="descriptionHtml"
+ class="md gl-mb-5 gl-min-h-8"
+ @change="toggleCheckboxes"
+ ></div>
+ </div>
+</template>
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 af9b8c6101a..7e9fa24e3f5 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,4 +1,5 @@
<script>
+import { isEmpty } from 'lodash';
import {
GlAlert,
GlSkeletonLoader,
@@ -11,6 +12,7 @@ import {
} from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
import { s__ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -27,12 +29,13 @@ import {
WIDGET_TYPE_ITERATION,
} from '../constants';
-import workItemQuery from '../graphql/work_item.query.graphql';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
+import workItemMilestoneSubscription from '../graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
+import { getWorkItemQuery } from '../utils';
import WorkItemActions from './work_item_actions.vue';
import WorkItemState from './work_item_state.vue';
@@ -72,6 +75,7 @@ export default {
WorkItemMilestone,
},
mixins: [glFeatureFlagMixin()],
+ inject: ['fullPath'],
props: {
isModal: {
type: Boolean,
@@ -83,6 +87,11 @@ export default {
required: false,
default: null,
},
+ iid: {
+ type: String,
+ required: false,
+ default: null,
+ },
workItemParentId: {
type: String,
required: false,
@@ -100,20 +109,26 @@ export default {
},
apollo: {
workItem: {
- query: workItemQuery,
+ query() {
+ return getWorkItemQuery(this.fetchByIid);
+ },
variables() {
- return {
- id: this.workItemId,
- };
+ return this.queryVariables;
},
skip() {
return !this.workItemId;
},
+ update(data) {
+ const workItem = this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ return workItem ?? {};
+ },
error() {
- this.error = this.$options.i18n.fetchError;
- document.title = s__('404|Not found');
+ this.setEmptyState();
},
result() {
+ if (isEmpty(this.workItem)) {
+ this.setEmptyState();
+ }
if (!this.isModal && this.workItem.project) {
const path = this.workItem.project?.fullPath
? ` · ${this.workItem.project.fullPath}`
@@ -127,30 +142,44 @@ export default {
document: workItemTitleSubscription,
variables() {
return {
- issuableId: this.workItemId,
+ issuableId: this.workItem.id,
};
},
+ skip() {
+ return !this.workItem?.id;
+ },
},
{
document: workItemDatesSubscription,
variables() {
return {
- issuableId: this.workItemId,
+ issuableId: this.workItem.id,
};
},
skip() {
- return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
+ return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE) || !this.workItem?.id;
},
},
{
document: workItemAssigneesSubscription,
variables() {
return {
- issuableId: this.workItemId,
+ issuableId: this.workItem.id,
+ };
+ },
+ skip() {
+ return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES) || !this.workItem?.id;
+ },
+ },
+ {
+ document: workItemMilestoneSubscription,
+ variables() {
+ return {
+ issuableId: this.workItem.id,
};
},
skip() {
- return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
+ return !this.isWidgetPresent(WIDGET_TYPE_MILESTONE) || !this.workItem?.id;
},
},
],
@@ -212,7 +241,20 @@ export default {
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
workItemMilestone() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
+ return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
+ },
+ fetchByIid() {
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(this.$route.query.iid_path);
+ },
+ queryVariables() {
+ return this.fetchByIid
+ ? {
+ fullPath: this.fullPath,
+ iid: this.iid,
+ }
+ : {
+ id: this.workItemId,
+ };
},
},
beforeDestroy() {
@@ -231,7 +273,7 @@ export default {
this.updateInProgress = true;
let updateMutation = updateWorkItemMutation;
let inputVariables = {
- id: this.workItemId,
+ id: this.workItem.id,
confidential: confidentialStatus,
};
@@ -240,7 +282,7 @@ export default {
inputVariables = {
id: this.parentWorkItem.id,
taskData: {
- id: this.workItemId,
+ id: this.workItem.id,
confidential: confidentialStatus,
},
};
@@ -275,6 +317,10 @@ export default {
this.updateInProgress = false;
});
},
+ setEmptyState() {
+ this.error = this.$options.i18n.fetchError;
+ document.title = s__('404|Not found');
+ },
},
WORK_ITEM_VIEWED_STORAGE_KEY,
};
@@ -352,7 +398,7 @@ export default {
:can-update="canUpdate"
:is-confidential="workItem.confidential"
:is-parent-confidential="parentWorkItemConfidentiality"
- @deleteWorkItem="$emit('deleteWorkItem', workItemType)"
+ @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
/>
@@ -406,6 +452,8 @@ export default {
:work-item-id="workItem.id"
:can-update="canUpdate"
:full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
@error="updateError = $event"
/>
<work-item-due-date
@@ -421,8 +469,10 @@ export default {
<work-item-milestone
v-if="workItemMilestone"
:work-item-id="workItem.id"
- :work-item-milestone="workItemMilestone.nodes[0]"
+ :work-item-milestone="workItemMilestone.milestone"
:work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
:can-update="canUpdate"
:full-path="fullPath"
@error="updateError = $event"
@@ -435,6 +485,8 @@ export default {
:weight="workItemWeight.weight"
:work-item-id="workItem.id"
:work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
@error="updateError = $event"
/>
<template v-if="workItemsMvc2Enabled">
@@ -445,6 +497,9 @@ export default {
:can-update="canUpdate"
:work-item-id="workItem.id"
:work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
@error="updateError = $event"
/>
</template>
@@ -452,6 +507,8 @@ export default {
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
:full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
class="gl-pt-5"
@error="updateError = $event"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index eae11c2bb2f..9ee302855c7 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -134,12 +134,12 @@ export default {
async clickShowDueDate() {
this.showDueDateInput = true;
await this.$nextTick();
- this.$refs.dueDatePicker.calendar.show();
+ this.$refs.dueDatePicker.show();
},
async clickShowStartDate() {
this.showStartDateInput = true;
await this.$nextTick();
- this.$refs.startDatePicker.calendar.show();
+ this.$refs.startDatePicker.show();
},
handleStartDateInput() {
if (this.dirtyDueDate && this.dirtyStartDate > this.dirtyDueDate) {
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 05077862690..22af3c653e9 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -8,7 +8,7 @@ import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/labe
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
-import workItemQuery from '../graphql/work_item.query.graphql';
+import { getWorkItemQuery } from '../utils';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import {
@@ -50,6 +50,15 @@ export default {
type: String,
required: true,
},
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -64,11 +73,14 @@ export default {
},
apollo: {
workItem: {
- query: workItemQuery,
+ query() {
+ return getWorkItemQuery(this.fetchByIid);
+ },
variables() {
- return {
- id: this.workItemId,
- };
+ return this.queryVariables;
+ },
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
},
skip() {
return !this.workItemId;
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 37aa48be6e5..0251dcc33fa 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -6,10 +6,6 @@ import WorkItemLinks from './work_item_links.vue';
Vue.use(GlToast);
export default function initWorkItemLinks() {
- if (!window.gon.features.workItemsHierarchy) {
- return;
- }
-
const workItemLinksRoot = document.querySelector('.js-work-item-links-root');
if (!workItemLinksRoot) {
@@ -21,7 +17,6 @@ export default function initWorkItemLinks() {
wiHasIssueWeightsFeature,
iid,
wiHasIterationsFeature,
- projectNamespace,
} = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
@@ -38,7 +33,6 @@ export default function initWorkItemLinks() {
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
- projectNamespace,
},
render: (createElement) =>
createElement('work-item-links', {
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 0d3e951de7e..3d469b790a1 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
@@ -1,5 +1,13 @@
<script>
-import { GlButton, GlIcon, GlAlert, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlAlert,
+ GlLoadingIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { produce } from 'immer';
import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -9,7 +17,12 @@ import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_detail
import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
-import { WIDGET_ICONS, WORK_ITEM_STATUS_TEXT, WIDGET_TYPE_HIERARCHY } from '../../constants';
+import {
+ FORM_TYPES,
+ WIDGET_ICONS,
+ WORK_ITEM_STATUS_TEXT,
+ WIDGET_TYPE_HIERARCHY,
+} from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import workItemQuery from '../../graphql/work_item.query.graphql';
@@ -20,6 +33,8 @@ import WorkItemLinksForm from './work_item_links_form.vue';
export default {
components: {
GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlIcon,
GlAlert,
GlLoadingIcon,
@@ -80,6 +95,7 @@ export default {
prefetchedWorkItem: null,
error: undefined,
parentIssue: null,
+ formType: null,
};
},
computed: {
@@ -89,6 +105,9 @@ export default {
issuableIteration() {
return this.parentIssue?.iteration;
},
+ issuableMilestone() {
+ return this.parentIssue?.milestone;
+ },
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@@ -125,9 +144,10 @@ export default {
toggle() {
this.isOpen = !this.isOpen;
},
- showAddForm() {
+ showAddForm(formType) {
this.isOpen = true;
this.isShownAddForm = true;
+ this.formType = formType;
this.$nextTick(() => {
this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();
});
@@ -239,15 +259,18 @@ export default {
'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.',
),
addChildButtonLabel: s__('WorkItem|Add'),
+ addChildOptionLabel: s__('WorkItem|Existing task'),
+ createChildOptionLabel: s__('WorkItem|New task'),
},
WIDGET_TYPE_TASK_ICON: WIDGET_ICONS.TASK,
WORK_ITEM_STATUS_TEXT,
+ FORM_TYPES,
};
</script>
<template>
<div
- class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-5"
+ class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4"
data-testid="work-item-links"
>
<div
@@ -264,15 +287,26 @@ export default {
{{ childrenCountLabel }}
</span>
</div>
- <gl-button
+ <gl-dropdown
v-if="canUpdate"
- category="secondary"
+ right
size="small"
- data-testid="toggle-add-form"
- @click="showAddForm"
+ :text="$options.i18n.addChildButtonLabel"
+ data-testid="toggle-form"
>
- {{ $options.i18n.addChildButtonLabel }}
- </gl-button>
+ <gl-dropdown-item
+ data-testid="toggle-create-form"
+ @click="showAddForm($options.FORM_TYPES.create)"
+ >
+ {{ $options.i18n.createChildOptionLabel }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ data-testid="toggle-add-form"
+ @click="showAddForm($options.FORM_TYPES.add)"
+ >
+ {{ $options.i18n.addChildOptionLabel }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
<gl-button
category="tertiary"
@@ -309,6 +343,8 @@ export default {
:children-ids="childrenIds"
:parent-confidential="confidential"
:parent-iteration="issuableIteration"
+ :parent-milestone="issuableMilestone"
+ :form-type="formType"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>
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 a01f4616cab..095ea86e0d8 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
@@ -1,21 +1,26 @@
<script>
-import { GlAlert, GlFormGroup, GlForm, GlFormCombobox, GlButton, GlFormInput } from '@gitlab/ui';
+import { GlAlert, GlFormGroup, GlForm, GlTokenSelector, GlButton, GlFormInput } 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__ } from '~/locale';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import projectWorkItemTypesQuery from '~/work_items/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 { TASK_TYPE_NAME } from '../../constants';
+import { FORM_TYPES, TASK_TYPE_NAME } from '../../constants';
export default {
components: {
GlAlert,
GlForm,
- GlFormCombobox,
+ GlTokenSelector,
GlButton,
GlFormGroup,
GlFormInput,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'hasIterationsFeature'],
props: {
issuableGid: {
@@ -38,6 +43,15 @@ export default {
required: false,
default: () => {},
},
+ parentMilestone: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ formType: {
+ type: String,
+ required: true,
+ },
},
apollo: {
workItemTypes: {
@@ -51,33 +65,73 @@ export default {
return data.workspace?.workItemTypes?.nodes;
},
},
+ availableWorkItems: {
+ query: projectWorkItemsQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ searchTerm: this.search?.title || this.search,
+ types: ['TASK'],
+ in: this.search ? 'TITLE' : undefined,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter((wi) => !this.childrenIds.includes(wi.id));
+ },
+ },
},
data() {
return {
+ workItemTypes: [],
availableWorkItems: [],
search: '',
+ searchStarted: false,
error: null,
childToCreateTitle: null,
+ workItemsToAdd: [],
};
},
computed: {
- actionsList() {
- return [
- {
- label: this.$options.i18n.createChildOptionLabel,
- fn: () => {
- this.childToCreateTitle = this.search?.title || this.search;
- },
+ workItemInput() {
+ let workItemInput = {
+ title: this.search?.title || this.search,
+ projectPath: this.projectPath,
+ workItemTypeId: this.taskWorkItemType,
+ hierarchyWidget: {
+ parentId: this.issuableGid,
},
- ];
+ confidential: this.parentConfidential,
+ };
+
+ if (this.associateMilestone) {
+ workItemInput = {
+ ...workItemInput,
+ milestoneWidget: {
+ milestoneId: this.parentMilestoneId,
+ },
+ };
+ }
+ return workItemInput;
+ },
+ workItemsMvc2Enabled() {
+ return this.glFeatures.workItemsMvc2;
+ },
+ isCreateForm() {
+ return this.formType === FORM_TYPES.create;
},
addOrCreateButtonLabel() {
- return this.childToCreateTitle
- ? this.$options.i18n.createChildOptionLabel
- : this.$options.i18n.addTaskButtonLabel;
+ if (this.isCreateForm) {
+ return this.$options.i18n.createChildOptionLabel;
+ } else if (this.workItemsToAdd.length > 1) {
+ return this.$options.i18n.addTasksButtonLabel;
+ }
+ return this.$options.i18n.addTaskButtonLabel;
},
addOrCreateMethod() {
- return this.childToCreateTitle ? this.createChild : this.addChild;
+ return this.isCreateForm ? this.createChild : this.addChild;
},
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
@@ -85,6 +139,24 @@ export default {
parentIterationId() {
return this.parentIteration?.id;
},
+ associateIteration() {
+ return this.parentIterationId && this.hasIterationsFeature && this.workItemsMvc2Enabled;
+ },
+ parentMilestoneId() {
+ return this.parentMilestone?.id;
+ },
+ associateMilestone() {
+ return this.parentMilestoneId && this.workItemsMvc2Enabled;
+ },
+ isSubmitButtonDisabled() {
+ return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
getIdFromGraphQLId,
@@ -92,6 +164,7 @@ export default {
this.error = null;
},
addChild() {
+ this.searchStarted = false;
this.$apollo
.mutate({
mutation: updateWorkItemMutation,
@@ -99,7 +172,7 @@ export default {
input: {
id: this.issuableGid,
hierarchyWidget: {
- childrenIds: [this.search.id],
+ childrenIds: this.workItemsToAdd.map((wi) => wi.id),
},
},
},
@@ -109,7 +182,7 @@ export default {
[this.error] = data.workItemUpdate.errors;
} else {
this.unsetError();
- this.$emit('addWorkItemChild', this.search);
+ this.workItemsToAdd = [];
}
})
.catch(() => {
@@ -124,15 +197,7 @@ export default {
.mutate({
mutation: createWorkItemMutation,
variables: {
- input: {
- title: this.search?.title || this.search,
- projectPath: this.projectPath,
- workItemTypeId: this.taskWorkItemType,
- hierarchyWidget: {
- parentId: this.issuableGid,
- },
- confidential: this.parentConfidential,
- },
+ input: this.workItemInput,
},
})
.then(({ data }) => {
@@ -145,7 +210,7 @@ export default {
* call update mutation only when there is an iteration associated with the issue
*/
// TODO: setting the iteration should be moved to the creation mutation once the backend is done
- if (this.parentIterationId && this.hasIterationsFeature) {
+ if (this.associateIteration) {
this.addIterationToWorkItem(data.workItemCreate.workItem.id);
}
}
@@ -171,10 +236,25 @@ export default {
},
});
},
+ 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'),
addTaskButtonLabel: s__('WorkItem|Add task'),
+ addTasksButtonLabel: s__('WorkItem|Add tasks'),
addChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to add a child. Please try again.',
),
@@ -182,7 +262,8 @@ export default {
createChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to create a child. Please try again.',
),
- placeholder: s__('WorkItem|Add a title'),
+ createPlaceholder: s__('WorkItem|Add a title'),
+ addPlaceholder: s__('WorkItem|Search existing tasks'),
fieldValidationMessage: __('Maximum of 255 characters'),
},
};
@@ -191,56 +272,59 @@ export default {
<template>
<gl-form
class="gl-bg-white gl-mb-3 gl-p-4 gl-border gl-border-gray-100 gl-rounded-base"
- @submit.prevent="createChild"
+ @submit.prevent="addOrCreateMethod"
>
<gl-alert v-if="error" variant="danger" class="gl-mb-3" @dismiss="unsetError">
{{ error }}
</gl-alert>
- <!-- Follow up issue to turn this functionality back on https://gitlab.com/gitlab-org/gitlab/-/issues/368757 -->
- <gl-form-combobox
- v-if="false"
- v-model="search"
- :token-list="availableWorkItems"
- match-value-to-attr="title"
- class="gl-mb-4"
- :label-text="$options.i18n.inputLabel"
- :action-list="actionsList"
- label-sr-only
- autofocus
- >
- <template #result="{ item }">
- <div class="gl-display-flex">
- <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
- <div>{{ item.title }}</div>
- </div>
- </template>
- <template #action="{ item }">
- <span class="gl-text-blue-500">{{ item.label }}</span>
- </template>
- </gl-form-combobox>
<gl-form-group
+ v-if="isCreateForm"
:label="$options.i18n.inputLabel"
:description="$options.i18n.fieldValidationMessage"
>
<gl-form-input
ref="wiTitleInput"
v-model="search"
- :placeholder="$options.i18n.placeholder"
+ :placeholder="$options.i18n.createPlaceholder"
maxlength="255"
class="gl-mb-3"
autofocus
/>
</gl-form-group>
+ <gl-token-selector
+ v-else
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="$options.i18n.addPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ class="gl-mb-4"
+ 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>
<gl-button
category="primary"
variant="confirm"
size="small"
type="submit"
- :disabled="search.length === 0"
+ :disabled="isSubmitButtonDisabled"
data-testid="add-child-button"
class="gl-mr-2"
>
- {{ $options.i18n.createChildOptionLabel }}
+ {{ addOrCreateButtonLabel }}
</gl-button>
<gl-button category="secondary" size="small" @click="$emit('cancel')">
{{ s__('WorkItem|Cancel') }}
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index c4a36e36555..a8d3b57aae0 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -11,10 +11,10 @@ import {
import * as Sentry from '@sentry/browser';
import { debounce } from 'lodash';
import Tracking from '~/tracking';
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
-import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
@@ -33,6 +33,7 @@ export default {
MILESTONE_FETCH_ERROR: s__(
'WorkItem|Something went wrong while fetching milestones. Please try again.',
),
+ EXPIRED_TEXT: __('(expired)'),
},
components: {
GlFormGroup,
@@ -68,6 +69,15 @@ export default {
type: String,
required: true,
},
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -90,8 +100,13 @@ export default {
emptyPlaceholder() {
return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
},
+ expired() {
+ return this.localMilestone?.expired ? ` ${this.$options.i18n.EXPIRED_TEXT}` : '';
+ },
dropdownText() {
- return this.localMilestone?.title || this.emptyPlaceholder;
+ return this.localMilestone?.title
+ ? `${this.localMilestone?.title}${this.expired}`
+ : this.emptyPlaceholder;
},
isLoadingMilestones() {
return this.$apollo.queries.milestones.loading;
@@ -106,6 +121,14 @@ export default {
};
},
},
+ watch: {
+ workItemMilestone: {
+ handler(newVal) {
+ this.localMilestone = newVal;
+ },
+ deep: true,
+ },
+ },
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
@@ -160,12 +183,13 @@ export default {
this.updateInProgress = true;
this.$apollo
.mutate({
- mutation: localUpdateWorkItemMutation,
+ mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
- milestone: {
- milestoneId: this.localMilestone?.id,
+ milestoneWidget: {
+ milestoneId:
+ this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id,
},
},
},
@@ -240,6 +264,7 @@ export default {
@click="handleMilestoneClick(milestone)"
>
{{ milestone.title }}
+ <template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template>
</gl-dropdown-item>
</template>
<gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 7737c535650..8b47c24de7d 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -102,4 +102,9 @@ export const WORK_ITEMS_TYPE_MAP = {
},
};
+export const FORM_TYPES = {
+ create: 'create',
+ add: 'add',
+};
+
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
diff --git a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
index 6edb6c89f16..daeb58c0947 100644
--- a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
@@ -4,6 +4,9 @@ query issuableDetails($fullPath: ID!, $iid: String) {
issuable: issue(iid: $iid) {
id
confidential
+ milestone {
+ id
+ }
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
new file mode 100644
index 00000000000..58140aff89e
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
@@ -0,0 +1,5 @@
+fragment MilestoneFragment on Milestone {
+ expired
+ id
+ title
+}
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 7d38d203b84..3a23db3886a 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -1,13 +1,16 @@
-query projectWorkItems($searchTerm: String, $projectPath: ID!, $types: [IssueType!]) {
+query projectWorkItems(
+ $searchTerm: String
+ $projectPath: ID!
+ $types: [IssueType!]
+ $in: [IssuableSearchableField!]
+) {
workspace: project(fullPath: $projectPath) {
id
- workItems(search: $searchTerm, types: $types) {
- edges {
- node {
- id
- title
- state
- }
+ workItems(search: $searchTerm, types: $types, in: $in) {
+ nodes {
+ id
+ title
+ state
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 36779dfe11e..fda71fabe22 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,6 +1,5 @@
enum LocalWidgetType {
ASSIGNEES
- MILESTONE
}
interface LocalWorkItemWidget {
@@ -12,11 +11,6 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
-type LocalWorkItemMilestone implements LocalWorkItemWidget {
- type: LocalWidgetType!
- nodes: [Milestone!]
-}
-
extend type WorkItem {
mockWidgets: [LocalWorkItemWidget]
}
@@ -29,14 +23,9 @@ input LocalUserInput {
avatarUrl: String
}
-input LocalMilestoneInput {
- milestoneId: ID!
-}
-
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [LocalUserInput!]
- milestone: LocalMilestoneInput!
}
type LocalWorkItemPayload {
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index bb05c9b2135..6a81cc230b1 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -2,6 +2,7 @@
fragment WorkItem on WorkItem {
id
+ iid
title
state
description
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index fa0ab56df75..3b46fed97ec 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -3,16 +3,5 @@
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
- mockWidgets @client {
- ... on LocalWorkItemMilestone {
- type
- nodes {
- id
- title
- expired
- dueDate
- }
- }
- }
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql
new file mode 100644
index 00000000000..4c3be007d96
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_by_iid.query.graphql
@@ -0,0 +1,12 @@
+#import "./work_item.fragment.graphql"
+
+query workItemByIid($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ ...WorkItem
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql
new file mode 100644
index 00000000000..4eb3d8067d9
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql
@@ -0,0 +1,20 @@
+subscription issuableDescription($issuableId: IssuableID!) {
+ issuableDescriptionUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetDescription {
+ type
+ description
+ descriptionHtml
+ lastEditedAt
+ lastEditedBy {
+ id
+ name
+ webPath
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql
new file mode 100644
index 00000000000..f5163003fe5
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql
@@ -0,0 +1,17 @@
+#import "~/work_items/graphql/milestone.fragment.graphql"
+
+subscription issuableMilestone($issuableId: IssuableID!) {
+ issuableMilestoneUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ ...MilestoneFragment
+ }
+ }
+ }
+ }
+ }
+}
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 d404cfb10ed..b9715c21c27 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
@@ -1,5 +1,6 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/work_items/graphql/milestone.fragment.graphql"
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
@@ -49,4 +50,10 @@ fragment WorkItemWidgets on WorkItemWidget {
}
}
}
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ ...MilestoneFragment
+ }
+ }
}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index f872d8c6b12..4fbcdfe2b96 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,13 +6,7 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const {
- fullPath,
- hasIssueWeightsFeature,
- issuesListPath,
- projectNamespace,
- hasIterationsFeature,
- } = el.dataset;
+ const { fullPath, hasIssueWeightsFeature, issuesListPath, hasIterationsFeature } = el.dataset;
return new Vue({
el,
@@ -23,7 +17,6 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
issuesListPath,
- projectNamespace,
hasIterationsFeature: parseBoolean(hasIterationsFeature),
},
render(createElement) {
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 4908b99e5b0..2245f984174 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -3,10 +3,11 @@ import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { getPreferredLocales, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
-import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
+import { getWorkItemQuery } from '../utils';
import ItemTitle from '../components/item_title.vue';
@@ -21,6 +22,7 @@ export default {
ItemTitle,
GlFormSelect,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['fullPath'],
props: {
initialTitle: {
@@ -71,6 +73,9 @@ export default {
return sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemType);
},
+ fetchByIid() {
+ return this.glFeatures.useIidInWorkItemsPath;
+ },
},
methods: {
async createWorkItem() {
@@ -89,28 +94,47 @@ export default {
workItemTypeId: this.selectedWorkItemType,
},
},
- update(store, { data: { workItemCreate } }) {
+ update: (store, { data: { workItemCreate } }) => {
const { workItem } = workItemCreate;
+ const data = this.fetchByIid
+ ? {
+ workspace: {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Project',
+ id: workItem.project.id,
+ workItems: {
+ __typename: 'WorkItemConnection',
+ nodes: [workItem],
+ },
+ },
+ }
+ : { workItem };
store.writeQuery({
- query: workItemQuery,
- variables: {
- id: workItem.id,
- },
- data: {
- workItem,
- },
+ query: getWorkItemQuery(this.fetchByIid),
+ variables: this.fetchByIid
+ ? {
+ fullPath: this.fullPath,
+ iid: workItem.iid,
+ }
+ : {
+ id: workItem.id,
+ },
+ data,
});
},
});
const {
data: {
workItemCreate: {
- workItem: { id },
+ workItem: { id, iid },
},
},
} = response;
- this.$router.push({ name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } });
+ const routerParams = this.fetchByIid
+ ? { name: 'workItem', params: { id: iid }, query: { iid_path: 'true' } }
+ : { name: 'workItem', params: { id: `${getIdFromGraphQLId(id)}` } };
+ this.$router.push(routerParams);
} catch {
this.error = this.createErrorText;
}
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index a2cacd8bd7a..1c00bd16263 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -38,14 +38,12 @@ export default {
this.ZenMode = new ZenMode();
},
methods: {
- deleteWorkItem(workItemType) {
+ deleteWorkItem({ workItemType, workItemId: id }) {
this.$apollo
.mutate({
mutation: deleteWorkItemMutation,
variables: {
- input: {
- id: this.gid,
- },
+ input: { id },
},
})
.then(({ data: { workItemDelete, errors } }) => {
@@ -72,6 +70,6 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
- <work-item-detail :work-item-id="gid" @deleteWorkItem="deleteWorkItem($event)" />
+ <work-item-detail :work-item-id="gid" :iid="id" @deleteWorkItem="deleteWorkItem($event)" />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js
index 2b39a298720..777badeb5be 100644
--- a/app/assets/javascripts/work_items/router/index.js
+++ b/app/assets/javascripts/work_items/router/index.js
@@ -9,7 +9,7 @@ Vue.use(VueRouter);
export function createRouter(fullPath) {
return new VueRouter({
- routes,
+ routes: routes(),
mode: 'history',
base: joinPaths(fullPath, '-', 'work_items'),
});
diff --git a/app/assets/javascripts/work_items/router/routes.js b/app/assets/javascripts/work_items/router/routes.js
index 95772bbd026..1e3a7e184bb 100644
--- a/app/assets/javascripts/work_items/router/routes.js
+++ b/app/assets/javascripts/work_items/router/routes.js
@@ -1,13 +1,22 @@
-export const routes = [
- {
- path: '/new',
- name: 'createWorkItem',
- component: () => import('../pages/create_work_item.vue'),
- },
- {
- path: '/:id',
- name: 'workItem',
- component: () => import('../pages/work_item_root.vue'),
- props: true,
- },
-];
+function getRoutes() {
+ const routes = [
+ {
+ path: '/:id',
+ name: 'workItem',
+ component: () => import('../pages/work_item_root.vue'),
+ props: true,
+ },
+ ];
+
+ if (gon.features?.workItemsMvc2) {
+ routes.unshift({
+ path: '/new',
+ name: 'createWorkItem',
+ component: () => import('../pages/create_work_item.vue'),
+ });
+ }
+
+ return routes;
+}
+
+export const routes = getRoutes;
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
new file mode 100644
index 00000000000..17f9c882c2d
--- /dev/null
+++ b/app/assets/javascripts/work_items/utils.js
@@ -0,0 +1,6 @@
+import workItemQuery from './graphql/work_item.query.graphql';
+import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
+
+export function getWorkItemQuery(isFetchedByIid) {
+ return isFetchedByIid ? workItemByIidQuery : workItemQuery;
+}
diff --git a/app/assets/javascripts/work_items_hierarchy/components/app.vue b/app/assets/javascripts/work_items_hierarchy/components/app.vue
index 779bd27516a..1eeb3abf4bd 100644
--- a/app/assets/javascripts/work_items_hierarchy/components/app.vue
+++ b/app/assets/javascripts/work_items_hierarchy/components/app.vue
@@ -25,7 +25,7 @@ export default {
workItemTypes() {
return this.workItemHierarchy.reduce(
(itemTypes, item) => {
- const skipItem = workItemTypes[item.type].isWorkItem && !window.gon?.features?.workItems;
+ const skipItem = workItemTypes[item.type].isWorkItem;
if (skipItem) {
return itemTypes;
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 21d9db26382..6878e9a10d7 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -1,4 +1,3 @@
-@import './pages/branches';
@import './pages/colors';
@import './pages/commits';
@import './pages/detail_page';
@@ -9,11 +8,11 @@
@import './pages/issues';
@import './pages/labels';
@import './pages/login';
+@import './pages/ml_experiment_tracking';
@import './pages/merge_requests';
@import './pages/monitor';
@import './pages/note_form';
@import './pages/notes';
-@import './pages/notifications';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/projects';
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index e69d7b4462d..27e9a041145 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,6 +1,4 @@
.user-contrib-cell {
- stroke: $t-gray-a-08;
-
&:hover {
cursor: pointer;
stroke: $black;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d561a7d9450..c5a34ca4b31 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -46,6 +46,10 @@
}
}
+ &.dropdown-reduced-height {
+ max-height: $dropdown-max-height;
+ }
+
@include media-breakpoint-down(xs) {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index a31910e3090..68a3493670d 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -8,6 +8,7 @@ gl-emoji {
}
.user-status-emoji {
+ margin-left: $gl-padding-4;
margin-right: $gl-padding-4;
gl-emoji {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 07516275e58..b28302f29ef 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -290,6 +290,10 @@
padding: $gl-padding;
}
}
+
+ .content-visibility-auto {
+ content-visibility: auto;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 02b76b89482..7a92adf7b7b 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -207,7 +207,7 @@ body {
}
@include media-breakpoint-up(sm) {
- .logged-out-marketing-header-candidate {
+ .logged-out-marketing-header {
--header-height: 72px;
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 900cf9fa4db..ea741af918c 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -1,15 +1,3 @@
-.ajax-users-select {
- width: 400px;
-
- &.input-large {
- width: 210px;
- }
-
- &.input-clamp {
- max-width: 100%;
- }
-}
-
.group-result {
.group-image {
float: left;
@@ -49,7 +37,3 @@
}
}
}
-
-.ajax-users-dropdown {
- min-width: 250px !important;
-}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 39786aa0138..14971e3b2ee 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -24,6 +24,10 @@
+ .snippet-file-content {
@include gl-mt-5;
}
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
}
.snippet-header {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 9cfc5a0201e..99284ea0a64 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -88,14 +88,6 @@ $white-normal: #f0f0f0 !default;
$white-dark: #eaeaea !default;
$white-transparent: rgba($white, 0.8) !default;
-$gray-lightest: #fdfdfd !default;
-$gray-light: #fafafa !default;
-$gray-lighter: #f9f9f9 !default;
-$gray-normal: #f5f5f5 !default;
-$gray-dark: darken($gray-light, $darken-dark-factor) !default;
-$gray-darker: #eee !default;
-$gray-darkest: #c4c4c4 !default;
-
$purple: #6d49cb !default;
$purple-light: #ede8fb !default;
@@ -103,11 +95,6 @@ $black: #000 !default;
$black-transparent: rgba(0, 0, 0, 0.3) !default;
$almost-black: #242424 !default;
-$t-gray-a-02: rgba($black, 0.02) !default;
-$t-gray-a-04: rgba($black, 0.04) !default;
-$t-gray-a-06: rgba($black, 0.06) !default;
-$t-gray-a-08: rgba($black, 0.08) !default;
-
$green-50: #ecf4ee !default;
$green-100: #c3e6cd !default;
$green-200: #91d4a8 !default;
@@ -168,18 +155,33 @@ $purple-800: #453894 !default;
$purple-900: #2f2a6b !default;
$purple-950: #232150 !default;
-$gray-10: #f5f5f5 !default;
-$gray-50: #f0f0f0 !default;
-$gray-100: #dbdbdb !default;
-$gray-200: #bfbfbf !default;
-$gray-300: #999 !default;
-$gray-400: #868686 !default;
-$gray-500: #666 !default;
-$gray-600: #5e5e5e !default;
-$gray-700: #525252 !default;
-$gray-800: #404040 !default;
-$gray-900: #303030 !default;
-$gray-950: #1f1f1f !default;
+$gray-10: #fbfafd !default;
+$gray-50: #ececef !default;
+$gray-100: #dcdcde !default;
+$gray-200: #bfbfc3 !default;
+$gray-300: #a4a3a8 !default;
+$gray-400: #89888d !default;
+$gray-500: #737278 !default;
+$gray-600: #626168 !default;
+$gray-700: #535158 !default;
+$gray-800: #434248 !default;
+$gray-900: #333238 !default;
+$gray-950: #1f1e24 !default;
+
+$gray-lightest: lighten($gray-10, 1) !default;
+$gray-light: $gray-10 !default;
+$gray-lighter: lighten($gray-50, 4) !default;
+$gray-normal: lighten($gray-50, 2) !default;
+$gray-dark: darken($gray-light, $darken-dark-factor) !default;
+$gray-darker: $gray-50 !default;
+$gray-darkest: $gray-200 !default;
+
+$t-gray-a-02: rgba($gray-950, 0.02) !default;
+$t-gray-a-04: rgba($gray-950, 0.04) !default;
+$t-gray-a-06: rgba($gray-950, 0.06) !default;
+$t-gray-a-08: rgba($gray-950, 0.08) !default;
+$t-gray-a-16: rgba($gray-950, 0.16) !default;
+$t-gray-a-24: rgba($gray-950, 0.24) !default;
$greens: (
'50': $green-50,
@@ -346,6 +348,20 @@ $theme-light-red-500: #c24b38;
$theme-light-red-600: #b03927;
$theme-light-red-700: #a62e21;
+// Data visualization color palette
+
+$data-viz-blue-50: #e9ebff;
+$data-viz-blue-100: #d4dcfa;
+$data-viz-blue-200: #b7c6ff;
+$data-viz-blue-300: #97acff;
+$data-viz-blue-400: #748eff;
+$data-viz-blue-500: #5772ff;
+$data-viz-blue-600: #445cf2;
+$data-viz-blue-700: #3547de;
+$data-viz-blue-800: #232fcf;
+$data-viz-blue-900: #1e23a8;
+$data-viz-blue-950: #11118a;
+
$border-white-light: darken($white, $darken-border-factor) !default;
$border-white-normal: darken($white-normal, $darken-border-factor) !default;
@@ -356,7 +372,7 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
/*
* UI elements
*/
-$contextual-sidebar-bg-color: #f5f5f5;
+$contextual-sidebar-bg-color: $gray-10;
$contextual-sidebar-border-color: #e9e9e9;
$border-color: $gray-100;
$shadow-color: $t-gray-a-08;
@@ -660,18 +676,7 @@ $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 boards.scss and are used for calculation of the board height.
- They probably should be derived in a smarter way.
-*/
$issue-boards-filter-height: 68px;
-$issue-boards-filter-height-md: 110px;
-$issue-boards-filter-height-sm: 299px;
-$issue-boards-breadcrumbs-height-xs: 63px;
-$issue-board-list-difference-xs: calc(#{$header-height} + #{$issue-boards-breadcrumbs-height-xs});
-$issue-board-list-difference-sm: calc(#{$header-height} + #{$breadcrumb-min-height});
-$issue-board-list-difference-md: calc(#{$issue-board-list-difference-sm} + #{$issue-boards-filter-height-md});
-$issue-board-list-difference-lg: calc(#{$issue-board-list-difference-sm} + #{$issue-boards-filter-height});
/*
The following heights are used in environment_logs.scss and are used for calculation of the log viewer height.
*/
@@ -710,11 +715,11 @@ $job-arrow-margin: 55px;
*/
// See https://gitlab.com/gitlab-org/gitlab/-/issues/332150 to align with Pajamas Design System
$calendar-activity-colors: (
- #f5f5f5,
- #d4dcfa,
- #748eff,
- #3547de,
- #11118a,
+ $gray-50,
+ $data-viz-blue-100,
+ $data-viz-blue-400,
+ $data-viz-blue-700,
+ $data-viz-blue-950,
) !default;
/*
@@ -756,12 +761,6 @@ $document-index-color: #888;
$help-shortcut-header-color: #333;
/*
-* Issues
-*/
-$issues-today-bg: #f3fff2 !default;
-$issues-today-border: #e1e8d5 !default;
-
-/*
* Label
*/
$label-font-size: 12px;
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 7fb2bf9a875..3438a73eff6 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -125,6 +125,14 @@ $dark-il: #de935f;
}
.code.dark {
+ // Highlight.js theme overrides (https://gitlab.com/gitlab-org/gitlab/-/issues/365167)
+ // We should be able to remove the overrides once the upstream issue is fixed (https://github.com/sourcegraph/sourcegraph/issues/23251)
+ @include hljs-override('title\\.class', $dark-nc);
+ @include hljs-override('title\\.class\\.inherited', $dark-no);
+ @include hljs-override('variable\\.constant', $dark-no);
+ @include hljs-override('title\\.function', $dark-nf);
+
+
// Line numbers
.file-line-num {
@include line-link($white, 'link');
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 66cada9181c..75dd342393d 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -104,7 +104,7 @@ $monokai-gh: #75715e;
@include hljs-override('selector-tag', $monokai-nt);
@include hljs-override('keyword', $monokai-k);
@include hljs-override('variable', $monokai-nv);
- @include hljs-override('variable.language_', $monokai-k);
+ @include hljs-override('variable\\.language_', $monokai-k);
@include hljs-override('title', $monokai-nf);
@include hljs-override('name', $monokai-k);
@include hljs-override('tag', $monokai-nt);
@@ -116,7 +116,13 @@ $monokai-gh: #75715e;
@include hljs-override('bullet', $monokai-n);
@include hljs-override('subst', $monokai-p);
@include hljs-override('symbol', $monokai-ss);
- @include hljs-override('title.class_.inherited__', $monokai-no);
+ @include hljs-override('title\\.class_\\.inherited__', $monokai-no);
+ @include hljs-override('title\\.class\\.inherited', $monokai-no);
+ @include hljs-override('title\\.class', $monokai-nc);
+ @include hljs-override('title\\.function', $monokai-nf);
+ @include hljs-override('variable\\.constant', $monokai-no);
+ @include hljs-override('variable\\.language', $monokai-nb);
+ @include hljs-override('params', $monokai-nb);
// Line numbers
.file-line-num {
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index a1bba8720a2..c0b2fb90aa0 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -107,7 +107,9 @@ $solarized-dark-il: #2aa198;
@include hljs-override('selector-tag', $solarized-dark-nt);
@include hljs-override('keyword', $solarized-dark-k);
@include hljs-override('variable', $solarized-dark-nv);
- @include hljs-override('variable.language_', $solarized-dark-k);
+ @include hljs-override('variable\\.language_', $solarized-dark-k);
+ @include hljs-override('variable\\.language', $solarized-dark-k);
+ @include hljs-override('variable\\.constant', $solarized-dark-no);
@include hljs-override('title', $solarized-dark-nf);
@include hljs-override('name', $solarized-dark-k);
@include hljs-override('tag', $solarized-dark-nt);
@@ -119,7 +121,11 @@ $solarized-dark-il: #2aa198;
@include hljs-override('bullet', $solarized-dark-n);
@include hljs-override('subst', $solarized-dark-p);
@include hljs-override('symbol', $solarized-dark-ni);
- @include hljs-override('title.class_.inherited__', $solarized-dark-no);
+ @include hljs-override('title\\.class_\\.inherited__', $solarized-dark-no);
+ @include hljs-override('title\\.class', $solarized-dark-nc);
+ @include hljs-override('title\\.class\\.inherited', $solarized-dark-no);
+ @include hljs-override('title\\.function', $solarized-dark-nf);
+ @include hljs-override('params', $solarized-dark-nb);
// Line numbers
.file-line-num {
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index 33945f7cda9..921b36dd610 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -106,7 +106,17 @@ $solarized-light-il: #2aa198;
}
.code.solarized-light {
- @include hljs-override('title.class_.inherited__', $solarized-light-no);
+ // Highlight.js theme overrides (https://gitlab.com/gitlab-org/gitlab/-/issues/365167)
+ // We should be able to remove the overrides once the upstream issue is fixed (https://github.com/sourcegraph/sourcegraph/issues/23251)
+ @include hljs-override('keyword', $solarized-light-k);
+ @include hljs-override('title\\.class_\\.inherited__', $solarized-light-no);
+ @include hljs-override('title\\.class\\.inherited', $solarized-light-no);
+ @include hljs-override('title\\.class', $solarized-light-nc);
+ @include hljs-override('title\\.function', $solarized-light-nf);
+ @include hljs-override('variable\\.constant', $solarized-light-no);
+ @include hljs-override('variable\\.language', $solarized-light-nb);
+ @include hljs-override('params', $solarized-light-nb);
+
// Line numbers
.file-line-num {
@include line-link($black, 'link');
diff --git a/app/assets/stylesheets/highlight/themes/white.scss b/app/assets/stylesheets/highlight/themes/white.scss
index b0f6595feff..f6cce25671f 100644
--- a/app/assets/stylesheets/highlight/themes/white.scss
+++ b/app/assets/stylesheets/highlight/themes/white.scss
@@ -2,9 +2,18 @@
@import '../white_base';
@include conflict-colors('white');
+
+ // Highlight.js theme overrides (https://gitlab.com/gitlab-org/gitlab/-/issues/365167)
+ // We should be able to remove the overrides once the upstream issue is fixed (https://github.com/sourcegraph/sourcegraph/issues/23251)
@include hljs-override('variable', $white-nv);
@include hljs-override('symbol', $white-ss);
- @include hljs-override('title.class_.inherited__', $white-no);
+ @include hljs-override('title\\.class_\\.inherited__', $white-no);
+ @include hljs-override('title\\.class\\.inherited', $white-no);
+ @include hljs-override('title\\.class', $white-nc);
+ @include hljs-override('variable\\.constant', $white-no);
+ @include hljs-override('variable\\.language', $white-nb);
+ @include hljs-override('title\\.function', $white-nf);
+ @include hljs-override('params', $white-nb);
}
:root {
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index d45bc865da5..0cc1fb40e4a 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -18,38 +18,9 @@
.boards-list,
.board-swimlanes {
- height: calc(100vh - #{$issue-board-list-difference-xs});
overflow-x: scroll;
min-height: 200px;
border-left: 8px solid var(--gray-10, $white);
-
- @include media-breakpoint-only(sm) {
- height: calc(100vh - #{$issue-board-list-difference-sm});
- }
-
- @include media-breakpoint-up(md) {
- height: calc(100vh - #{$issue-board-list-difference-md});
- }
-
- @include media-breakpoint-up(lg) {
- height: calc(100vh - #{$issue-board-list-difference-lg});
- }
-
- .with-performance-bar & {
- height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
-
- @include media-breakpoint-only(sm) {
- height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
- }
-
- @include media-breakpoint-up(md) {
- height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
- }
-
- @include media-breakpoint-up(lg) {
- height: calc(100vh - #{$issue-board-list-difference-lg} - #{$performance-bar-height});
- }
- }
}
.board {
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/page_bundles/branches.scss
index 18158fab75f..2aa90529e22 100644
--- a/app/assets/stylesheets/pages/branches.scss
+++ b/app/assets/stylesheets/page_bundles/branches.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.branch-info {
flex: auto;
min-width: 0;
diff --git a/app/assets/stylesheets/page_bundles/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss
index a877ae72e31..4f29ff4b1ad 100644
--- a/app/assets/stylesheets/page_bundles/clusters.scss
+++ b/app/assets/stylesheets/page_bundles/clusters.scss
@@ -20,3 +20,7 @@
min-height: 372px;
}
}
+
+.cluster-button-container:focus-within {
+ @include gl-focus;
+}
diff --git a/app/assets/stylesheets/page_bundles/dashboard.scss b/app/assets/stylesheets/page_bundles/dashboard.scss
new file mode 100644
index 00000000000..986a9cc530d
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/dashboard.scss
@@ -0,0 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
+.empty-state .svg-250 img {
+ max-width: $gl-spacing-scale-20;
+}
diff --git a/app/assets/stylesheets/page_bundles/design_management.scss b/app/assets/stylesheets/page_bundles/design_management.scss
new file mode 100644
index 00000000000..143682e1cd7
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/design_management.scss
@@ -0,0 +1,215 @@
+@import 'mixins_and_variables_and_functions';
+
+$design-pin-diameter: 28px;
+$design-pin-diameter-sm: 24px;
+$t-gray-a-16-design-pin: rgba($black, 0.16);
+
+.design-card-header {
+ background: transparent;
+}
+
+.design-checkbox {
+ position: absolute;
+ top: $gl-padding;
+ left: 30px;
+}
+
+.layout-page.design-detail-layout {
+ max-height: 100vh;
+}
+
+.design-detail {
+ background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
+
+ .with-performance-bar & {
+ top: 35px;
+ }
+
+ .comment-indicator {
+ border-radius: 50%;
+ }
+
+ .comment-indicator,
+ .frame .design-note-pin {
+ &:active {
+ cursor: grabbing;
+ }
+ }
+}
+
+.design-list-item {
+ height: 280px;
+ text-decoration: none;
+
+ .icon-version-status {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+
+ .card-body {
+ height: 230px;
+ }
+}
+
+// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197
+.design-list-item-new {
+ height: 210px;
+}
+
+.design-note-pin {
+ display: flex;
+ height: $design-pin-diameter;
+ width: $design-pin-diameter;
+ box-sizing: content-box;
+ background-color: var(--purple-500, $purple-500);
+ color: var(--white, $white);
+ font-weight: $gl-font-weight-bold;
+ border-radius: 50%;
+ z-index: 1;
+ padding: 0;
+ border: 0;
+
+ &.draft {
+ background-color: var(--orange-500, $orange-500);
+ }
+
+ &.resolved {
+ background-color: var(--gray-500, $gray-500);
+ }
+
+ &.on-image {
+ box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
+ border: var(--white, $white) 2px solid;
+ will-change: transform, box-shadow, opacity;
+ // NOTE: verbose transition property required for Safari
+ transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
+ transform-origin: 0 0;
+ transform: translate(-50%, -50%);
+
+ &:hover {
+ transform: scale(1.2) translate(-50%, -50%);
+ }
+
+ &:active {
+ box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
+ }
+
+ &.inactive {
+ @include gl-opacity-5;
+
+ &:hover {
+ @include gl-opacity-10;
+ }
+ }
+ }
+
+ &.small {
+ position: absolute;
+ border: 1px solid var(--white, $white);
+ height: $design-pin-diameter-sm;
+ width: $design-pin-diameter-sm;
+ }
+
+ &.user-avatar {
+ top: 25px;
+ right: 8px;
+ }
+}
+
+.design-scaler-wrapper {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.image-notes {
+ overflow-y: scroll;
+ padding: $gl-padding;
+ padding-top: 50px;
+ background-color: var(--white, $white);
+ flex-shrink: 0;
+ min-width: 400px;
+ flex-basis: 28%;
+
+ .link-inherit-color {
+ &:hover,
+ &:active,
+ &:focus {
+ color: inherit;
+ text-decoration: none;
+ }
+ }
+
+ .toggle-comments {
+ line-height: 20px;
+ border-top: 1px solid var(--border-color, $border-color);
+
+ &.expanded {
+ border-bottom: 1px solid var(--border-color, $border-color);
+ }
+
+ .toggle-comments-button:focus {
+ text-decoration: none;
+ color: var(--blue-600, $blue-600);
+ }
+ }
+
+ .design-note-pin {
+ margin-left: $gl-padding;
+ }
+
+ .design-discussion {
+ margin: $gl-padding 0;
+
+ &::before {
+ content: '';
+ border-left: 1px solid var(--gray-100, $gray-100);
+ position: absolute;
+ left: 28px;
+ top: -17px;
+ height: 17px;
+ }
+
+ .design-note {
+ padding: $gl-padding;
+ list-style: none;
+ transition: background $gl-transition-duration-medium $general-hover-transition-curve;
+ border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
+ border-top-right-radius: $border-radius-default;
+
+ a {
+ color: inherit;
+ }
+
+ .note-text a {
+ color: var(--blue-600, $blue-600);
+ }
+ }
+
+ .reply-wrapper {
+ padding: $gl-padding;
+ }
+ }
+
+ .reply-wrapper {
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+
+ .new-discussion-disclaimer {
+ line-height: 20px;
+ }
+}
+
+@media (max-width: map-get($grid-breakpoints, lg)) {
+ .design-detail {
+ overflow-y: scroll;
+ }
+
+ .image-notes {
+ overflow-y: auto;
+ min-width: 100%;
+ flex-grow: 1;
+ flex-basis: auto;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 3951f72112f..ec75c53d026 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -568,6 +568,11 @@ $ide-commit-header-height: 48px;
text-decoration: line-through;
}
}
+
+ .gl-form-radio,
+ .gl-form-checkbox {
+ color: var(--ide-text-color, $gl-text-color);
+ }
}
.ide-sidebar-link {
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index bbdcf1ea0c6..26d694f7421 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -1,9 +1,5 @@
@import 'mixins_and_variables_and_functions';
-$design-pin-diameter: 28px;
-$design-pin-diameter-sm: 24px;
-$t-gray-a-16-design-pin: rgba($black, 0.16);
-
.description {
li {
position: relative;
@@ -27,216 +23,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
}
}
-.design-card-header {
- background: transparent;
-}
-
-.design-checkbox {
- position: absolute;
- top: $gl-padding;
- left: 30px;
-}
-
-.layout-page.design-detail-layout {
- max-height: 100vh;
-}
-
-.design-detail {
- background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
-
- .with-performance-bar & {
- top: 35px;
- }
-
- .comment-indicator {
- border-radius: 50%;
- }
-
- .comment-indicator,
- .frame .design-note-pin {
- &:active {
- cursor: grabbing;
- }
- }
-}
-
-.design-list-item {
- height: 280px;
- text-decoration: none;
-
- .icon-version-status {
- position: absolute;
- right: 10px;
- top: 10px;
- }
-
- .card-body {
- height: 230px;
- }
-}
-
-// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197
-.design-list-item-new {
- height: 210px;
-}
-
-.design-note-pin {
- display: flex;
- height: $design-pin-diameter;
- width: $design-pin-diameter;
- box-sizing: content-box;
- background-color: var(--purple-500, $purple-500);
- color: var(--white, $white);
- font-weight: $gl-font-weight-bold;
- border-radius: 50%;
- z-index: 1;
- padding: 0;
- border: 0;
-
- &.draft {
- background-color: var(--orange-500, $orange-500);
- }
-
- &.resolved {
- background-color: var(--gray-500, $gray-500);
- }
-
- &.on-image {
- box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
- border: var(--white, $white) 2px solid;
- will-change: transform, box-shadow, opacity;
- // NOTE: verbose transition property required for Safari
- transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
- transform-origin: 0 0;
- transform: translate(-50%, -50%);
-
- &:hover {
- transform: scale(1.2) translate(-50%, -50%);
- }
-
- &:active {
- box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
- }
-
- &.inactive {
- @include gl-opacity-5;
-
- &:hover {
- @include gl-opacity-10;
- }
- }
- }
-
- &.small {
- position: absolute;
- border: 1px solid var(--white, $white);
- height: $design-pin-diameter-sm;
- width: $design-pin-diameter-sm;
- }
-
- &.user-avatar {
- top: 25px;
- right: 8px;
- }
-}
-
-.design-scaler-wrapper {
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
-}
-
-.image-notes {
- overflow-y: scroll;
- padding: $gl-padding;
- padding-top: 50px;
- background-color: var(--white, $white);
- flex-shrink: 0;
- min-width: 400px;
- flex-basis: 28%;
-
- .link-inherit-color {
- &:hover,
- &:active,
- &:focus {
- color: inherit;
- text-decoration: none;
- }
- }
-
- .toggle-comments {
- line-height: 20px;
- border-top: 1px solid var(--border-color, $border-color);
-
- &.expanded {
- border-bottom: 1px solid var(--border-color, $border-color);
- }
-
- .toggle-comments-button:focus {
- text-decoration: none;
- color: var(--blue-600, $blue-600);
- }
- }
-
- .design-note-pin {
- margin-left: $gl-padding;
- }
-
- .design-discussion {
- margin: $gl-padding 0;
-
- &::before {
- content: '';
- border-left: 1px solid var(--gray-100, $gray-100);
- position: absolute;
- left: 28px;
- top: -17px;
- height: 17px;
- }
-
- .design-note {
- padding: $gl-padding;
- list-style: none;
- transition: background $gl-transition-duration-medium $general-hover-transition-curve;
- border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
- border-top-right-radius: $border-radius-default;
-
- a {
- color: inherit;
- }
-
- .note-text a {
- color: var(--blue-600, $blue-600);
- }
- }
-
- .reply-wrapper {
- padding: $gl-padding;
- }
- }
-
- .reply-wrapper {
- border-top: 1px solid var(--border-color, $border-color);
- }
-
- .new-discussion-disclaimer {
- line-height: 20px;
- }
-}
-
-@media (max-width: map-get($grid-breakpoints, lg)) {
- .design-detail {
- overflow-y: scroll;
- }
-
- .image-notes {
- overflow-y: auto;
- min-width: 100%;
- flex-grow: 1;
- flex-basis: auto;
- }
-}
-
.is-ghost {
opacity: 0.3;
pointer-events: none;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index b2fbce7cb4b..771428b49e0 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -269,7 +269,7 @@ $tabs-holder-z-index: 250;
position: -webkit-sticky;
position: sticky;
- top: var(--top-pos);
+ top: calc(var(--top-pos) + var(--performance-bar-height, 0px));
max-height: calc(100vh - var(--top-pos) - var(--system-header-height, 0px) - var(--performance-bar-height, 0px) - var(--review-bar-height, 0px));
.drag-handle {
@@ -391,6 +391,10 @@ $tabs-holder-z-index: 250;
text-overflow: ellipsis;
min-width: 100px;
+ display: grid;
+ grid-template-columns: max-content minmax(0, max-content) max-content;
+ grid-gap: $gl-spacing-scale-2;
+
@include media-breakpoint-up(xs) {
min-width: 0;
max-width: 100%;
@@ -404,6 +408,7 @@ $tabs-holder-z-index: 250;
.deploy-heading,
.merge-train-position-indicator {
+ padding: $gl-padding-8;
@include media-breakpoint-up(md) {
padding: $gl-padding-8 $gl-padding;
}
@@ -476,6 +481,19 @@ $tabs-holder-z-index: 250;
border-radius: $border-radius-default;
background: var(--white, $white);
+ > .mr-widget-section {
+ > :first-child {
+ border-top-left-radius: $border-radius-default - 1px;
+ border-top-right-radius: $border-radius-default - 1px;
+ }
+
+ > :last-child,
+ .deploy-heading:last-child {
+ border-bottom-left-radius: $border-radius-default - 1px;
+ border-bottom-right-radius: $border-radius-default - 1px;
+ }
+ }
+
> .mr-widget-border-top:first-of-type {
border-top: 0;
}
@@ -637,7 +655,6 @@ $tabs-holder-z-index: 250;
word-break: break-all;
}
- .deploy-link,
.label-branch {
&.label-truncate {
// NOTE: This selector targets its children because some of the HTML comes from
@@ -808,6 +825,13 @@ $tabs-holder-z-index: 250;
.mr-widget-border-top {
border-top: 1px solid var(--border-color, $border-color);
+
+ &:last-child {
+ .report-block-container {
+ border-bottom-left-radius: $border-radius-default - 1px;
+ border-bottom-right-radius: $border-radius-default - 1px;
+ }
+ }
}
.mr-widget-extension {
@@ -875,9 +899,12 @@ $tabs-holder-z-index: 250;
}
.mr-widget-workflow {
- margin-top: $gl-padding;
position: relative;
+ &:not(:first-child) {
+ margin-top: $gl-padding;
+ }
+
&:not(:last-child)::before {
content: '';
border-left: 2px solid var(--gray-10, $gray-10);
@@ -1078,6 +1105,31 @@ $tabs-holder-z-index: 250;
border-top: 0;
}
+.mr-widget-status-icon-level-1::before {
+ @include gl-content-empty;
+ @include gl-absolute;
+ @include gl-left-0;
+ @include gl-top-0;
+ @include gl-bottom-0;
+ @include gl-right-0;
+ @include gl-opacity-3;
+ @include gl-rounded-full;
+ @include gl-border-solid;
+ @include gl-border-4;
+}
+
+.mr-widget-status-icon-level-1::after {
+ @include gl-content-empty;
+ @include gl-absolute;
+ @include gl-rounded-full;
+ @include gl-border-solid;
+ @include gl-border-4;
+ @include gl-left-2;
+ @include gl-right-2;
+ @include gl-top-2;
+ @include gl-bottom-2;
+}
+
.memory-graph-container {
background: var(--white, $white);
border: 1px solid var(--gray-100, $gray-100);
@@ -1154,3 +1206,19 @@ $tabs-holder-z-index: 250;
margin-bottom: 0;
}
}
+
+.commits ol:not(:last-of-type) {
+ margin-bottom: 0;
+}
+
+.mr-section-container {
+ .state-container-action-buttons {
+ @include media-breakpoint-up(md) {
+ flex-direction: row-reverse;
+
+ .btn {
+ margin-left: auto;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/page_bundles/notifications.scss
index 2fd2757cf08..88437954f4c 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/page_bundles/notifications.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.notification-list-item {
@include media-breakpoint-down(sm) {
.notification-dropdown {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6070311dcb6..1b6e7954366 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -76,10 +76,6 @@
.btn-edit {
margin-left: auto;
}
-
- .emoji-block {
- padding: $gl-padding-4 0;
- }
}
.issuable-show-labels {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index c88834c088f..c2ac4f32480 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -75,11 +75,6 @@ ul.related-merge-requests > li gl-emoji {
.merge-request,
.issue {
- &.today {
- background: $issues-today-bg;
- border-color: $issues-today-border;
- }
-
&.closed,
&.merged {
background: $gray-light;
@@ -123,9 +118,6 @@ ul.related-merge-requests > li gl-emoji {
}
.new-branch-col {
- @include gl-pb-3;
- @include gl-my-2;
-
.discussion-filter-container {
&:not(:last-child) {
margin-right: $gl-spacing-scale-3;
@@ -225,7 +217,7 @@ ul.related-merge-requests > li gl-emoji {
.new-branch-col {
@include gl-pb-0;
- align-self: center;
+ align-self: flex-end;
}
}
diff --git a/app/assets/stylesheets/pages/ml_experiment_tracking.scss b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
new file mode 100644
index 00000000000..2dff51cff92
--- /dev/null
+++ b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
@@ -0,0 +1,16 @@
+@import '../page_bundles/mixins_and_variables_and_functions';
+
+.ml-experiment-row {
+ .title {
+ margin-bottom: $gl-spacing-scale-1;
+ font-weight: $gl-font-weight-bold;
+ }
+
+ .ml-experiment-info {
+ color: $gl-text-color-secondary;
+ }
+
+ a {
+ color: $gl-text-color;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 438b7b1afa6..fa3c87490f1 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -6,7 +6,7 @@ $system-note-svg-size: 1rem;
content: '';
border-left: 2px solid var(--gray-10, $gray-10);
position: absolute;
- top: 0;
+ top: $gl-padding-6;
bottom: 0;
left: calc(#{$left} - 1px);
height: calc(100% + 1.5rem);
@@ -421,7 +421,7 @@ $system-note-svg-size: 1rem;
height: $system-note-icon-size;
border: 1px solid $gray-10;
border-radius: $system-note-icon-size;
- margin: -8px 0 0;
+ margin: -6px 0 0;
svg {
width: $system-note-svg-size;
@@ -968,7 +968,7 @@ $system-note-svg-size: 1rem;
height: 12px;
}
- &:hover,
+ &:hover:not([disabled]),
&.inverted {
&::before {
background-color: $white;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index be8707dcd50..bf20204cfd9 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -277,139 +277,128 @@
.description p {
margin-bottom: 0;
color: $gl-text-color-secondary;
+ @include str-truncated(100%);
}
}
.projects-list {
@include basic-list;
- display: flex;
- flex-direction: column;
+ @include gl-display-table;
.project-row {
- @include basic-list-stats;
- display: flex;
- align-items: center;
- padding: $gl-padding-12 0;
+ @include gl-display-table-row;
}
- h2 {
- font-size: $gl-font-size;
- font-weight: $gl-font-weight-bold;
- margin-bottom: 0;
-
- @include media-breakpoint-up(sm) {
- .namespace-name {
- font-weight: $gl-font-weight-normal;
- }
- }
+ .project-cell {
+ @include gl-display-table-cell;
+ @include gl-border-b;
+ @include gl-vertical-align-top;
+ @include gl-py-4;
}
- .avatar-container {
- flex: 0 0 auto;
- align-self: flex-start;
+ .project-row:last-of-type {
+ .project-cell {
+ @include gl-border-none;
+ }
}
- .project-details {
- min-width: 0;
- line-height: $gl-line-height;
- .flex-wrapper {
- min-width: 0;
- margin-top: -$gl-padding-8; // negative margin required for flex-wrap
- flex: 1 1 100%;
+ &.admin-projects,
+ &.group-settings-projects {
+ .project-row {
+ @include basic-list-stats;
- .project-title {
- line-height: 20px;
+ .description > p {
+ @include gl-mb-0;
}
}
+ .controls {
+ @include gl-line-height-42;
+ }
+ }
+
+ .project-details {
+ max-width: 625px;
+
p,
.commit-row-message {
+ @include gl-mb-0;
@include str-truncated(100%);
- margin-bottom: 0;
- }
-
- .user-access-role {
- margin: 0;
}
.description {
line-height: 1.5;
- color: $gl-text-color-secondary;
- }
-
- @include media-breakpoint-down(md) {
- .user-access-role {
- line-height: $gl-line-height-14;
- }
+ max-height: $gl-spacing-scale-8;
}
}
.ci-status-link {
- display: inline-block;
- line-height: 17px;
- vertical-align: middle;
-
- &:hover {
- text-decoration: none;
- }
+ @include gl-text-decoration-none;
}
- .controls {
- @include media-breakpoint-down(xs) {
- margin-top: $gl-padding-8;
+ &:not(.compact) {
+ .controls {
+ @include media-breakpoint-up(lg) {
+ @include gl-justify-content-start;
+ @include gl-pr-9;
+
+ &:not(.with-pipeline-status) {
+ .icon-wrapper:first-of-type {
+ @include media-breakpoint-up(lg) {
+ @include gl-ml-7;
+ }
+ }
+ }
+ }
}
- @include media-breakpoint-up(sm) {
- margin-top: 0;
+ .project-details {
+ p,
+ .commit-row-message {
+ @include gl-white-space-normal;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ /* stylelint-disable-next-line value-no-vendor-prefix */
+ display: -webkit-box;
+ }
}
+ }
- @include media-breakpoint-up(lg) {
- flex: 1 1 40%;
+ .controls {
+ @include media-breakpoint-up(sm) {
+ @include gl-justify-content-end;
}
.icon-wrapper {
- color: inherit;
- margin-right: $gl-padding;
-
@include media-breakpoint-down(md) {
- margin-right: 0;
- margin-left: $gl-padding-8;
+ @include gl-mr-0;
+ @include gl-ml-3;
}
@include media-breakpoint-down(xs) {
&:first-child {
- margin-left: 0;
- }
- }
- }
-
- &:not(.with-pipeline-status) {
- .icon-wrapper:first-of-type {
- @include media-breakpoint-up(lg) {
- margin-left: $gl-padding-32;
+ @include gl-ml-0;
}
}
}
.ci-status-link {
- display: inline-flex;
+ @include gl-display-inline-flex;
}
}
.icon-container {
- @include media-breakpoint-down(xs) {
- margin-right: $gl-padding-8;
+ @include media-breakpoint-up(lg) {
+ margin-right: $gl-spacing-scale-7;
}
}
&.compact {
- .project-row {
- padding: $gl-padding 0;
- }
-
- h2 {
- font-size: $gl-font-size;
+ .description {
+ @include gl-w-full;
+ @include gl-display-table;
+ @include gl-table-layout-fixed;
}
.avatar-container {
@@ -422,27 +411,15 @@
}
}
- .controls {
- @include media-breakpoint-up(sm) {
- margin-top: 0;
- }
- }
-
.updated-note {
@include media-breakpoint-up(sm) {
- margin-top: $gl-padding-8;
+ @include gl-mt-2;
}
}
.icon-wrapper {
- margin-left: $gl-padding-8;
- margin-right: 0;
-
- @include media-breakpoint-down(xs) {
- &:first-child {
- margin-left: 0;
- }
- }
+ @include gl-ml-3;
+ @include gl-mr-0;
}
.user-access-role {
@@ -451,10 +428,6 @@
}
@include media-breakpoint-down(md) {
- h2 {
- font-size: $gl-font-size;
- }
-
.avatar-container {
@include avatar-size(40px, 10px);
min-height: 40px;
@@ -468,24 +441,18 @@
@include media-breakpoint-down(md) {
.updated-note {
- margin-top: $gl-padding-8;
- text-align: right;
+ @include gl-mt-3;
+ @include gl-text-right;
}
}
.forks,
.pipeline-status,
.updated-note {
- display: flex;
+ @include gl-display-flex;
}
@include media-breakpoint-down(md) {
- &:not(.explore) {
- .forks {
- display: none;
- }
- }
-
&.explore {
.pipeline-status,
.updated-note {
@@ -496,8 +463,8 @@
@include media-breakpoint-down(xs) {
.updated-note {
- margin-top: 0;
- text-align: left;
+ @include gl-mt-0;
+ @include gl-text-left;
}
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index a8027d2a5f5..1bca04e5eb1 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -52,13 +52,6 @@ input[type='checkbox']:hover {
.global-search-container {
flex-grow: 1;
}
-
- @include media-breakpoint-down(lg) {
- .title-container {
- flex: 0;
- overflow: hidden;
- }
- }
}
}
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 32c3ce1ba8c..11131cc1a4b 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -6,15 +6,15 @@
color-scheme: dark;
}
body.gl-dark {
- --gray-10: #1f1f1f;
- --gray-50: #303030;
- --gray-100: #404040;
- --gray-200: #525252;
- --gray-700: #dbdbdb;
- --gray-900: #fafafa;
+ --gray-10: #1f1e24;
+ --gray-50: #333238;
+ --gray-100: #434248;
+ --gray-200: #535158;
+ --gray-700: #bfbfc3;
+ --gray-900: #ececef;
--green-100: #0d532a;
--green-700: #91d4a8;
- --gl-text-color: #fafafa;
+ --gl-text-color: #ececef;
--border-color: #4f4f4f;
--black: #fff;
}
@@ -42,9 +42,9 @@ body {
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
- color: #fafafa;
+ color: #ececef;
text-align: left;
- background-color: #1f1f1f;
+ background-color: #1f1e24;
}
ul {
margin-top: 0;
@@ -118,7 +118,7 @@ kbd {
padding: 0.2rem 0.4rem;
font-size: 90%;
color: #333;
- background-color: #fafafa;
+ background-color: #ececef;
border-radius: 0.2rem;
}
kbd kbd {
@@ -141,24 +141,24 @@ kbd kbd {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.5;
- color: #fafafa;
+ color: #ececef;
background-color: #333;
background-clip: padding-box;
- border: 1px solid #868686;
+ border: 1px solid #737278;
border-radius: 0.25rem;
}
@media (prefers-reduced-motion: reduce) {
}
.form-control:-moz-focusring {
color: transparent;
- text-shadow: 0 0 0 #fafafa;
+ text-shadow: 0 0 0 #ececef;
}
.form-control::placeholder {
- color: #bfbfbf;
+ color: #a4a3a8;
opacity: 1;
}
.form-control:disabled {
- background-color: #303030;
+ background-color: #333238;
opacity: 1;
}
.form-inline {
@@ -176,7 +176,7 @@ kbd kbd {
.btn {
display: inline-block;
font-weight: 400;
- color: #fafafa;
+ color: #ececef;
text-align: center;
vertical-align: middle;
user-select: none;
@@ -212,7 +212,7 @@ kbd kbd {
padding: 0.5rem 0;
margin: 0.125rem 0 0;
font-size: 1rem;
- color: #fafafa;
+ color: #ececef;
text-align: left;
list-style: none;
background-color: #333;
@@ -319,15 +319,15 @@ kbd kbd {
border-radius: 10rem;
}
.badge-success {
- color: #fff;
+ color: #fbfafd;
background-color: #2da160;
}
.badge-info {
- color: #fff;
+ color: #fbfafd;
background-color: #428fdc;
}
.badge-warning {
- color: #fff;
+ color: #fbfafd;
background-color: #c17d10;
}
.rounded-circle {
@@ -371,7 +371,7 @@ kbd kbd {
.gl-avatar {
border-width: 1px;
border-style: solid;
- border-color: rgba(0, 0, 0, 0.08);
+ border-color: rgba(251, 250, 253, 0.08);
overflow: hidden;
flex-shrink: 0;
}
@@ -455,8 +455,8 @@ a.gl-badge.badge-warning:active {
padding-left: 0.75rem;
padding-right: 0.75rem;
height: auto;
- color: #fafafa;
- box-shadow: inset 0 0 0 1px #868686;
+ color: #ececef;
+ box-shadow: inset 0 0 0 1px #737278;
border-style: none;
appearance: none;
-moz-appearance: none;
@@ -465,17 +465,17 @@ a.gl-badge.badge-warning:active {
.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
.gl-form-input.form-control:disabled,
.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #1f1f1f;
- box-shadow: inset 0 0 0 1px #404040;
+ background-color: #1f1e24;
+ box-shadow: inset 0 0 0 1px #434248;
}
.gl-form-input:disabled,
.gl-form-input.form-control:disabled {
cursor: not-allowed;
- color: #999;
+ color: #89888d;
}
.gl-form-input::placeholder,
.gl-form-input.form-control::placeholder {
- color: #868686;
+ color: #737278;
}
.gl-icon {
fill: currentColor;
@@ -518,9 +518,9 @@ a.gl-badge.badge-warning:active {
padding-right: 0.75rem;
background-color: transparent;
line-height: 1rem;
- color: #fafafa;
+ color: #ececef;
fill: currentColor;
- box-shadow: inset 0 0 0 1px #525252;
+ box-shadow: inset 0 0 0 1px #535158;
justify-content: center;
align-items: center;
font-size: 0.875rem;
@@ -531,20 +531,20 @@ a.gl-badge.badge-warning:active {
}
.gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #bfbfbf, 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333, 0 0 0 3px #1f75cb;
outline: none;
- background-color: #404040;
+ background-color: #434248;
}
.gl-button.gl-button.btn-default:active .gl-icon,
.gl-button.gl-button.btn-default.active .gl-icon {
- color: #fafafa;
+ color: #ececef;
}
.gl-button.gl-button.btn-default .gl-icon {
- color: #999;
+ color: #89888d;
}
.gl-search-box-by-type-search-icon {
margin: 0.5rem;
- color: #999;
+ color: #89888d;
width: 1rem;
position: absolute;
}
@@ -594,35 +594,40 @@ svg {
height: 0;
margin: 4px 0;
overflow: hidden;
- border-top: 1px solid #404040;
+ border-top: 1px solid #434248;
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #999;
+ color: #89888d;
}
html {
overflow-y: scroll;
}
+@media (min-width: 576px) {
+ .logged-out-marketing-header {
+ --header-height: 72px;
+ }
+}
.btn {
border-radius: 4px;
font-size: 0.875rem;
font-weight: 400;
padding: 6px 10px;
background-color: #333;
- border-color: #404040;
- color: #fafafa;
- color: #fafafa;
+ border-color: #434248;
+ color: #ececef;
+ color: #ececef;
white-space: nowrap;
}
.btn:active {
- background-color: #303030;
+ background-color: #333238;
box-shadow: none;
}
.btn:active,
.btn.active {
background-color: #444;
border-color: #4f4f4f;
- color: #fafafa;
+ color: #ececef;
}
.btn svg {
height: 15px;
@@ -634,7 +639,7 @@ html {
.badge.badge-pill:not(.gl-badge) {
font-weight: 400;
background-color: rgba(255, 255, 255, 0.07);
- color: #dbdbdb;
+ color: #bfbfc3;
vertical-align: baseline;
}
.gl-font-sm {
@@ -653,10 +658,10 @@ html {
.dropdown-menu-toggle {
padding: 6px 8px 6px 10px;
background-color: #333;
- color: #fafafa;
+ color: #ececef;
font-size: 14px;
text-align: left;
- border: 1px solid #404040;
+ border: 1px solid #434248;
border-radius: 0.25rem;
white-space: nowrap;
}
@@ -685,7 +690,7 @@ html {
font-weight: 400;
padding: 8px 0;
background-color: #333;
- border: 1px solid #404040;
+ border: 1px solid #434248;
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@@ -708,7 +713,7 @@ html {
font-weight: 400;
position: relative;
padding: 8px 12px;
- color: #fafafa;
+ color: #ececef;
line-height: 16px;
white-space: normal;
overflow: hidden;
@@ -718,7 +723,7 @@ html {
.dropdown-menu li > a:active,
.dropdown-menu li button:active {
background-color: #4f4f4f;
- color: #fafafa;
+ color: #ececef;
outline: 0;
text-decoration: none;
}
@@ -732,7 +737,7 @@ html {
height: 1px;
margin: 0.25rem 0;
padding: 0;
- background-color: #404040;
+ background-color: #434248;
}
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
@@ -759,7 +764,7 @@ html {
}
input {
border-radius: 0.25rem;
- color: #fafafa;
+ color: #ececef;
background-color: #333;
}
.form-control {
@@ -767,23 +772,23 @@ input {
padding: 6px 10px;
}
.form-control::placeholder {
- color: #868686;
+ color: #737278;
}
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 0.6875rem;
line-height: 10px;
- color: var(--gray-700, #dbdbdb);
+ color: var(--gray-700, #bfbfc3);
vertical-align: middle;
- background-color: var(--gray-10, #1f1f1f);
+ background-color: var(--gray-10, #1f1e24);
border-width: 1px;
border-style: solid;
- border-color: var(--gray-100, #404040) var(--gray-100, #404040)
- var(--gray-200, #525252);
+ border-color: var(--gray-100, #434248) var(--gray-100, #434248)
+ var(--gray-200, #535158);
border-image: none;
border-radius: 3px;
- box-shadow: 0 -1px 0 var(--gray-200, #525252) inset;
+ box-shadow: 0 -1px 0 var(--gray-200, #535158) inset;
}
.navbar-gitlab {
padding: 0 16px;
@@ -1037,7 +1042,7 @@ kbd {
width: 100%;
align-items: center;
padding: 10px 16px 10px 10px;
- color: #fafafa;
+ color: #ececef;
background-color: transparent;
border: 0;
text-align: left;
@@ -1049,7 +1054,7 @@ kbd {
.context-header .sidebar-context-title {
overflow: hidden;
text-overflow: ellipsis;
- color: #fafafa;
+ color: #ececef;
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
@@ -1073,7 +1078,7 @@ kbd {
z-index: 600;
width: 256px;
top: var(--header-height, 48px);
- background-color: #f5f5f5;
+ background-color: #1f1e24;
border-right: 1px solid #e9e9e9;
transform: translate3d(0, 0, 0);
}
@@ -1110,7 +1115,7 @@ kbd {
}
.nav-sidebar a {
text-decoration: none;
- color: #fafafa;
+ color: #ececef;
}
.nav-sidebar li {
white-space: nowrap;
@@ -1395,7 +1400,7 @@ kbd {
display: block;
}
.sidebar-top-level-items li > a.gl-link {
- color: #fafafa;
+ color: #ececef;
}
.sidebar-top-level-items li > a.gl-link:active {
text-decoration: none;
@@ -1412,12 +1417,12 @@ kbd {
.close-nav-button {
height: 48px;
padding: 0 16px;
- background-color: #303030;
+ background-color: #333238;
border: 0;
- color: #999;
+ color: #89888d;
display: flex;
align-items: center;
- background-color: #f5f5f5;
+ background-color: #1f1e24;
position: fixed;
bottom: 0;
width: 255px;
@@ -1488,14 +1493,14 @@ kbd {
}
}
input::-moz-placeholder {
- color: #868686;
+ color: #737278;
opacity: 1;
}
input::-ms-input-placeholder {
- color: #868686;
+ color: #737278;
}
input:-ms-input-placeholder {
- color: #868686;
+ color: #737278;
}
svg {
fill: currentColor;
@@ -1624,7 +1629,7 @@ svg.s16 {
padding: 0;
background: #222;
overflow: hidden;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
+ box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
}
.avatar.avatar-tile {
border-radius: 0;
@@ -1633,8 +1638,8 @@ svg.s16 {
.identicon {
text-align: center;
vertical-align: top;
- color: #fafafa;
- background-color: #303030;
+ color: #ececef;
+ background-color: #333238;
}
.identicon.s16 {
font-size: 10px;
@@ -1663,7 +1668,7 @@ svg.s16 {
background-color: #5c2900;
}
.identicon.bg7 {
- background-color: #303030;
+ background-color: #333238;
}
.avatar-container {
overflow: hidden;
@@ -1702,18 +1707,18 @@ svg.s16 {
color-scheme: dark;
}
body.gl-dark {
- --gray-10: #1f1f1f;
- --gray-50: #303030;
- --gray-100: #404040;
- --gray-200: #525252;
- --gray-300: #5e5e5e;
- --gray-400: #868686;
- --gray-500: #999;
- --gray-600: #bfbfbf;
- --gray-700: #dbdbdb;
- --gray-800: #f0f0f0;
- --gray-900: #fafafa;
- --gray-950: #fff;
+ --gray-10: #1f1e24;
+ --gray-50: #333238;
+ --gray-100: #434248;
+ --gray-200: #535158;
+ --gray-300: #626168;
+ --gray-400: #737278;
+ --gray-500: #89888d;
+ --gray-600: #a4a3a8;
+ --gray-700: #bfbfc3;
+ --gray-800: #dcdcde;
+ --gray-900: #ececef;
+ --gray-950: #fbfafd;
--green-50: #0a4020;
--green-100: #0d532a;
--green-200: #24663b;
@@ -1785,58 +1790,59 @@ body.gl-dark {
--dark-icon-color-purple-3: #9a79f7;
--dark-icon-color-orange-1: #665349;
--dark-icon-color-orange-2: #b37a5d;
- --gl-text-color: #fafafa;
+ --gl-text-color: #ececef;
--border-color: #4f4f4f;
--white: #333;
--black: #fff;
+ --gray-light: #333238;
--svg-status-bg: #333;
}
.nav-sidebar,
.toggle-sidebar-button,
.close-nav-button {
- background-color: #262626;
- border-right: 1px solid #303030;
+ background-color: #29282d;
+ border-right: 1px solid #333238;
}
.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
- background: rgba(255, 255, 255, 0.04);
+ background: rgba(251, 250, 253, 0.04);
}
.gl-avatar {
border-style: none;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
+ box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
}
body.gl-dark {
- --gl-theme-accent: #868686;
+ --gl-theme-accent: #737278;
}
body.gl-dark .navbar-gitlab {
- background-color: #fafafa;
+ background-color: #ececef;
}
body.gl-dark .navbar-gitlab .navbar-collapse {
- color: #fafafa;
+ color: #ececef;
}
body.gl-dark .navbar-gitlab .container-fluid .navbar-toggler {
- border-left: 1px solid #b3b3b3;
- color: #fafafa;
+ border-left: 1px solid #a3a2a6;
+ color: #ececef;
}
body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > a,
body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
body.gl-dark .navbar-gitlab .navbar-nav > li.active > a,
body.gl-dark .navbar-gitlab .navbar-nav > li.active > button {
- color: #fafafa;
+ color: #ececef;
background-color: #333;
}
body.gl-dark .navbar-gitlab .navbar-sub-nav {
- color: #fafafa;
+ color: #ececef;
}
body.gl-dark .navbar-gitlab .nav > li {
- color: #fafafa;
+ color: #ececef;
}
body.gl-dark .navbar-gitlab .nav > li.header-search-new {
- color: #fafafa;
+ color: #ececef;
}
body.gl-dark .navbar-gitlab .nav > li > a .notification-dot {
- border: 2px solid #fafafa;
+ border: 2px solid #ececef;
}
body.gl-dark
.navbar-gitlab
@@ -1844,7 +1850,7 @@ body.gl-dark
> li
> a.header-help-dropdown-toggle
.notification-dot {
- background-color: #fafafa;
+ background-color: #ececef;
}
body.gl-dark
.navbar-gitlab
@@ -1852,10 +1858,10 @@ body.gl-dark
> li
> a.header-user-dropdown-toggle
.header-user-avatar {
- border-color: #fafafa;
+ border-color: #ececef;
}
body.gl-dark .navbar-gitlab .nav > li.active > a {
- color: #fafafa;
+ color: #ececef;
background-color: #333;
}
body.gl-dark .navbar-gitlab .nav > li.active > a .notification-dot {
@@ -1867,48 +1873,48 @@ body.gl-dark
> li.active
> a.header-help-dropdown-toggle
.notification-dot {
- background-color: #fafafa;
+ background-color: #ececef;
}
body.gl-dark .header-search {
- background-color: rgba(250, 250, 250, 0.2) !important;
+ background-color: rgba(236, 236, 239, 0.2) !important;
border-radius: 4px;
}
body.gl-dark .header-search svg.gl-search-box-by-type-search-icon {
- color: rgba(250, 250, 250, 0.8);
+ color: rgba(236, 236, 239, 0.8);
}
body.gl-dark .header-search input {
background-color: transparent;
- color: rgba(250, 250, 250, 0.8);
- box-shadow: inset 0 0 0 1px rgba(250, 250, 250, 0.4);
+ color: rgba(236, 236, 239, 0.8);
+ box-shadow: inset 0 0 0 1px rgba(236, 236, 239, 0.4);
}
body.gl-dark .header-search input::placeholder {
- color: rgba(250, 250, 250, 0.8);
+ color: rgba(236, 236, 239, 0.8);
}
body.gl-dark .header-search input:active::placeholder {
- color: #868686;
+ color: #737278;
}
body.gl-dark .header-search .keyboard-shortcut-helper {
- color: #fafafa;
- background-color: rgba(250, 250, 250, 0.2);
+ color: #ececef;
+ background-color: rgba(236, 236, 239, 0.2);
}
body.gl-dark .search form {
- background-color: rgba(250, 250, 250, 0.2);
+ background-color: rgba(236, 236, 239, 0.2);
}
body.gl-dark .search .search-input::placeholder {
- color: rgba(250, 250, 250, 0.8);
+ color: rgba(236, 236, 239, 0.8);
}
body.gl-dark .search .search-input-wrap .search-icon,
body.gl-dark .search .search-input-wrap .clear-icon {
- fill: rgba(250, 250, 250, 0.8);
+ fill: rgba(236, 236, 239, 0.8);
}
body.gl-dark .nav-sidebar li.active > a {
- color: #fafafa;
+ color: #ececef;
}
body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
- background-color: var(--gray-100, #303030);
- color: var(--gray-900, #fafafa);
+ background-color: var(--gray-100, #333238);
+ color: var(--gray-900, #ececef);
}
body.gl-dark .navbar-gitlab {
background-color: var(--gray-50);
@@ -1945,18 +1951,18 @@ body.gl-dark .navbar-gitlab .search form .search-input {
color-scheme: dark;
}
body.gl-dark {
- --gray-10: #1f1f1f;
- --gray-50: #303030;
- --gray-100: #404040;
- --gray-200: #525252;
- --gray-300: #5e5e5e;
- --gray-400: #868686;
- --gray-500: #999;
- --gray-600: #bfbfbf;
- --gray-700: #dbdbdb;
- --gray-800: #f0f0f0;
- --gray-900: #fafafa;
- --gray-950: #fff;
+ --gray-10: #1f1e24;
+ --gray-50: #333238;
+ --gray-100: #434248;
+ --gray-200: #535158;
+ --gray-300: #626168;
+ --gray-400: #737278;
+ --gray-500: #89888d;
+ --gray-600: #a4a3a8;
+ --gray-700: #bfbfc3;
+ --gray-800: #dcdcde;
+ --gray-900: #ececef;
+ --gray-950: #fbfafd;
--green-50: #0a4020;
--green-100: #0d532a;
--green-200: #24663b;
@@ -2028,10 +2034,11 @@ body.gl-dark {
--dark-icon-color-purple-3: #9a79f7;
--dark-icon-color-orange-1: #665349;
--dark-icon-color-orange-2: #b37a5d;
- --gl-text-color: #fafafa;
+ --gl-text-color: #ececef;
--border-color: #4f4f4f;
--white: #333;
--black: #fff;
+ --gray-light: #333238;
--svg-status-bg: #333;
}
.tab-width-8 {
@@ -2054,9 +2061,19 @@ body.gl-dark {
.gl-display-none {
display: none;
}
+@media (min-width: 576px) {
+ .gl-sm-display-none {
+ display: none;
+ }
+}
.gl-display-flex {
display: flex;
}
+@media (min-width: 992px) {
+ .gl-lg-display-flex {
+ display: flex;
+ }
+}
@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
@@ -2067,9 +2084,6 @@ body.gl-dark {
display: block;
}
}
-.gl-display-inline-block\! {
- display: inline-block !important;
-}
.gl-align-items-center {
align-items: center;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 61a2ce8dd62..7fb373bb6f4 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -23,7 +23,7 @@ body {
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
- color: #303030;
+ color: #333238;
text-align: left;
background-color: #fff;
}
@@ -99,7 +99,7 @@ kbd {
padding: 0.2rem 0.4rem;
font-size: 90%;
color: #fff;
- background-color: #303030;
+ background-color: #333238;
border-radius: 0.2rem;
}
kbd kbd {
@@ -122,24 +122,24 @@ kbd kbd {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.5;
- color: #303030;
+ color: #333238;
background-color: #fff;
background-clip: padding-box;
- border: 1px solid #868686;
+ border: 1px solid #89888d;
border-radius: 0.25rem;
}
@media (prefers-reduced-motion: reduce) {
}
.form-control:-moz-focusring {
color: transparent;
- text-shadow: 0 0 0 #303030;
+ text-shadow: 0 0 0 #333238;
}
.form-control::placeholder {
- color: #5e5e5e;
+ color: #626168;
opacity: 1;
}
.form-control:disabled {
- background-color: #fafafa;
+ background-color: #fbfafd;
opacity: 1;
}
.form-inline {
@@ -157,7 +157,7 @@ kbd kbd {
.btn {
display: inline-block;
font-weight: 400;
- color: #303030;
+ color: #333238;
text-align: center;
vertical-align: middle;
user-select: none;
@@ -193,7 +193,7 @@ kbd kbd {
padding: 0.5rem 0;
margin: 0.125rem 0 0;
font-size: 1rem;
- color: #303030;
+ color: #333238;
text-align: left;
list-style: none;
background-color: #fff;
@@ -352,7 +352,7 @@ kbd kbd {
.gl-avatar {
border-width: 1px;
border-style: solid;
- border-color: rgba(0, 0, 0, 0.08);
+ border-color: rgba(31, 30, 36, 0.08);
overflow: hidden;
flex-shrink: 0;
}
@@ -436,8 +436,8 @@ a.gl-badge.badge-warning:active {
padding-left: 0.75rem;
padding-right: 0.75rem;
height: auto;
- color: #303030;
- box-shadow: inset 0 0 0 1px #868686;
+ color: #333238;
+ box-shadow: inset 0 0 0 1px #89888d;
border-style: none;
appearance: none;
-moz-appearance: none;
@@ -446,17 +446,17 @@ a.gl-badge.badge-warning:active {
.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
.gl-form-input.form-control:disabled,
.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #f5f5f5;
- box-shadow: inset 0 0 0 1px #dbdbdb;
+ background-color: #fbfafd;
+ box-shadow: inset 0 0 0 1px #dcdcde;
}
.gl-form-input:disabled,
.gl-form-input.form-control:disabled {
cursor: not-allowed;
- color: #666;
+ color: #737278;
}
.gl-form-input::placeholder,
.gl-form-input.form-control::placeholder {
- color: #868686;
+ color: #89888d;
}
.gl-icon {
fill: currentColor;
@@ -499,9 +499,9 @@ a.gl-badge.badge-warning:active {
padding-right: 0.75rem;
background-color: transparent;
line-height: 1rem;
- color: #303030;
+ color: #333238;
fill: currentColor;
- box-shadow: inset 0 0 0 1px #bfbfbf;
+ box-shadow: inset 0 0 0 1px #bfbfc3;
justify-content: center;
align-items: center;
font-size: 0.875rem;
@@ -512,20 +512,20 @@ a.gl-badge.badge-warning:active {
}
.gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
+ box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
- background-color: #dbdbdb;
+ background-color: #dcdcde;
}
.gl-button.gl-button.btn-default:active .gl-icon,
.gl-button.gl-button.btn-default.active .gl-icon {
- color: #303030;
+ color: #333238;
}
.gl-button.gl-button.btn-default .gl-icon {
- color: #666;
+ color: #737278;
}
.gl-search-box-by-type-search-icon {
margin: 0.5rem;
- color: #666;
+ color: #737278;
width: 1rem;
position: absolute;
}
@@ -575,35 +575,40 @@ svg {
height: 0;
margin: 4px 0;
overflow: hidden;
- border-top: 1px solid #dbdbdb;
+ border-top: 1px solid #dcdcde;
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #666;
+ color: #737278;
}
html {
overflow-y: scroll;
}
+@media (min-width: 576px) {
+ .logged-out-marketing-header {
+ --header-height: 72px;
+ }
+}
.btn {
border-radius: 4px;
font-size: 0.875rem;
font-weight: 400;
padding: 6px 10px;
background-color: #fff;
- border-color: #dbdbdb;
- color: #303030;
- color: #303030;
+ border-color: #dcdcde;
+ color: #333238;
+ color: #333238;
white-space: nowrap;
}
.btn:active {
- background-color: #f0f0f0;
+ background-color: #ececef;
box-shadow: none;
}
.btn:active,
.btn.active {
background-color: #eaeaea;
border-color: #e3e3e3;
- color: #303030;
+ color: #333238;
}
.btn svg {
height: 15px;
@@ -615,7 +620,7 @@ html {
.badge.badge-pill:not(.gl-badge) {
font-weight: 400;
background-color: rgba(0, 0, 0, 0.07);
- color: #525252;
+ color: #535158;
vertical-align: baseline;
}
.gl-font-sm {
@@ -634,10 +639,10 @@ html {
.dropdown-menu-toggle {
padding: 6px 8px 6px 10px;
background-color: #fff;
- color: #303030;
+ color: #333238;
font-size: 14px;
text-align: left;
- border: 1px solid #dbdbdb;
+ border: 1px solid #dcdcde;
border-radius: 0.25rem;
white-space: nowrap;
}
@@ -666,7 +671,7 @@ html {
font-weight: 400;
padding: 8px 0;
background-color: #fff;
- border: 1px solid #dbdbdb;
+ border: 1px solid #dcdcde;
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
@@ -689,7 +694,7 @@ html {
font-weight: 400;
position: relative;
padding: 8px 12px;
- color: #303030;
+ color: #333238;
line-height: 16px;
white-space: normal;
overflow: hidden;
@@ -698,8 +703,8 @@ html {
}
.dropdown-menu li > a:active,
.dropdown-menu li button:active {
- background-color: #eee;
- color: #303030;
+ background-color: #ececef;
+ color: #333238;
outline: 0;
text-decoration: none;
}
@@ -713,7 +718,7 @@ html {
height: 1px;
margin: 0.25rem 0;
padding: 0;
- background-color: #dbdbdb;
+ background-color: #dcdcde;
}
.dropdown-menu .badge.badge-pill + span:not(.badge):not(.badge-pill) {
margin-right: 40px;
@@ -740,7 +745,7 @@ html {
}
input {
border-radius: 0.25rem;
- color: #303030;
+ color: #333238;
background-color: #fff;
}
.form-control {
@@ -748,23 +753,23 @@ input {
padding: 6px 10px;
}
.form-control::placeholder {
- color: #868686;
+ color: #89888d;
}
kbd {
display: inline-block;
padding: 3px 5px;
font-size: 0.6875rem;
line-height: 10px;
- color: var(--gray-700, #525252);
+ color: var(--gray-700, #535158);
vertical-align: middle;
- background-color: var(--gray-10, #f5f5f5);
+ background-color: var(--gray-10, #fbfafd);
border-width: 1px;
border-style: solid;
- border-color: var(--gray-100, #dbdbdb) var(--gray-100, #dbdbdb)
- var(--gray-200, #bfbfbf);
+ border-color: var(--gray-100, #dcdcde) var(--gray-100, #dcdcde)
+ var(--gray-200, #bfbfc3);
border-image: none;
border-radius: 3px;
- box-shadow: 0 -1px 0 var(--gray-200, #bfbfbf) inset;
+ box-shadow: 0 -1px 0 var(--gray-200, #bfbfc3) inset;
}
.navbar-gitlab {
padding: 0 16px;
@@ -986,7 +991,7 @@ kbd {
float: left;
margin-right: 5px;
border-radius: 50%;
- border: 1px solid #f5f5f5;
+ border: 1px solid #f2f2f4;
}
.notification-dot {
background-color: #d99530;
@@ -1018,7 +1023,7 @@ kbd {
width: 100%;
align-items: center;
padding: 10px 16px 10px 10px;
- color: #303030;
+ color: #333238;
background-color: transparent;
border: 0;
text-align: left;
@@ -1030,7 +1035,7 @@ kbd {
.context-header .sidebar-context-title {
overflow: hidden;
text-overflow: ellipsis;
- color: #303030;
+ color: #333238;
}
@media (min-width: 768px) {
.page-with-contextual-sidebar {
@@ -1054,7 +1059,7 @@ kbd {
z-index: 600;
width: 256px;
top: var(--header-height, 48px);
- background-color: #f5f5f5;
+ background-color: #fbfafd;
border-right: 1px solid #e9e9e9;
transform: translate3d(0, 0, 0);
}
@@ -1091,7 +1096,7 @@ kbd {
}
.nav-sidebar a {
text-decoration: none;
- color: #303030;
+ color: #333238;
}
.nav-sidebar li {
white-space: nowrap;
@@ -1376,7 +1381,7 @@ kbd {
display: block;
}
.sidebar-top-level-items li > a.gl-link {
- color: #303030;
+ color: #333238;
}
.sidebar-top-level-items li > a.gl-link:active {
text-decoration: none;
@@ -1393,12 +1398,12 @@ kbd {
.close-nav-button {
height: 48px;
padding: 0 16px;
- background-color: #fafafa;
+ background-color: #fbfafd;
border: 0;
- color: #666;
+ color: #737278;
display: flex;
align-items: center;
- background-color: #f5f5f5;
+ background-color: #fbfafd;
position: fixed;
bottom: 0;
width: 255px;
@@ -1469,14 +1474,14 @@ kbd {
}
}
input::-moz-placeholder {
- color: #868686;
+ color: #89888d;
opacity: 1;
}
input::-ms-input-placeholder {
- color: #868686;
+ color: #89888d;
}
input:-ms-input-placeholder {
- color: #868686;
+ color: #89888d;
}
svg {
fill: currentColor;
@@ -1603,9 +1608,9 @@ svg.s16 {
width: 40px;
height: 40px;
padding: 0;
- background: #fdfdfd;
+ background: #fefefe;
overflow: hidden;
- box-shadow: inset 0 0 0 1px rgba(31, 31, 31, 0.1);
+ box-shadow: inset 0 0 0 1px rgba(31, 30, 36, 0.1);
}
.avatar.avatar-tile {
border-radius: 0;
@@ -1614,8 +1619,8 @@ svg.s16 {
.identicon {
text-align: center;
vertical-align: top;
- color: #303030;
- background-color: #f0f0f0;
+ color: #333238;
+ background-color: #ececef;
}
.identicon.s16 {
font-size: 10px;
@@ -1644,7 +1649,7 @@ svg.s16 {
background-color: #fdf1dd;
}
.identicon.bg7 {
- background-color: #f0f0f0;
+ background-color: #ececef;
}
.avatar-container {
overflow: hidden;
@@ -1700,9 +1705,19 @@ svg.s16 {
.gl-display-none {
display: none;
}
+@media (min-width: 576px) {
+ .gl-sm-display-none {
+ display: none;
+ }
+}
.gl-display-flex {
display: flex;
}
+@media (min-width: 992px) {
+ .gl-lg-display-flex {
+ display: flex;
+ }
+}
@media (min-width: 576px) {
.gl-sm-display-block {
display: block;
@@ -1713,9 +1728,6 @@ svg.s16 {
display: block;
}
}
-.gl-display-inline-block\! {
- display: inline-block !important;
-}
.gl-align-items-center {
align-items: center;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 33e10b9bd62..7ae158b3930 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -22,7 +22,7 @@ body {
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
- color: #303030;
+ color: #333238;
text-align: left;
background-color: #fff;
}
@@ -110,7 +110,7 @@ h3 {
margin-bottom: 0.25rem;
font-weight: 600;
line-height: 1.2;
- color: #303030;
+ color: #333238;
}
h1 {
font-size: 2.1875rem;
@@ -196,24 +196,24 @@ hr {
font-size: 0.875rem;
font-weight: 400;
line-height: 1.5;
- color: #303030;
+ color: #333238;
background-color: #fff;
background-clip: padding-box;
- border: 1px solid #868686;
+ border: 1px solid #89888d;
border-radius: 0.25rem;
}
@media (prefers-reduced-motion: reduce) {
}
.form-control:-moz-focusring {
color: transparent;
- text-shadow: 0 0 0 #303030;
+ text-shadow: 0 0 0 #333238;
}
.form-control::placeholder {
- color: #5e5e5e;
+ color: #626168;
opacity: 1;
}
.form-control:disabled {
- background-color: #fafafa;
+ background-color: #fbfafd;
opacity: 1;
}
.form-group {
@@ -222,7 +222,7 @@ hr {
.btn {
display: inline-block;
font-weight: 400;
- color: #303030;
+ color: #333238;
text-align: center;
vertical-align: middle;
user-select: none;
@@ -282,10 +282,10 @@ input.btn-block[type="button"] {
border-color: #b3d7ff;
}
.custom-control-input:disabled ~ .custom-control-label {
- color: #5e5e5e;
+ color: #626168;
}
.custom-control-input:disabled ~ .custom-control-label::before {
- background-color: #fafafa;
+ background-color: #fbfafd;
}
.custom-control-label {
position: relative;
@@ -302,7 +302,7 @@ input.btn-block[type="button"] {
pointer-events: none;
content: "";
background-color: #fff;
- border: #666 solid 1px;
+ border: #737278 solid 1px;
}
.custom-control-label::after {
position: absolute;
@@ -400,8 +400,8 @@ input.btn-block[type="button"] {
padding-left: 0.75rem;
padding-right: 0.75rem;
height: auto;
- color: #303030;
- box-shadow: inset 0 0 0 1px #868686;
+ color: #333238;
+ box-shadow: inset 0 0 0 1px #89888d;
border-style: none;
appearance: none;
-moz-appearance: none;
@@ -410,27 +410,27 @@ input.btn-block[type="button"] {
.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
.gl-form-input.form-control:disabled,
.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
- background-color: #f5f5f5;
- box-shadow: inset 0 0 0 1px #dbdbdb;
+ background-color: #fbfafd;
+ box-shadow: inset 0 0 0 1px #dcdcde;
}
.gl-form-input:disabled,
.gl-form-input.form-control:disabled {
cursor: not-allowed;
- color: #666;
+ color: #737278;
}
.gl-form-input::placeholder,
.gl-form-input.form-control::placeholder {
- color: #868686;
+ color: #89888d;
}
.gl-form-checkbox {
font-size: 0.875rem;
line-height: 1rem;
- color: #303030;
+ color: #333238;
}
.gl-form-checkbox .custom-control-input:disabled,
.gl-form-checkbox .custom-control-input:disabled ~ .custom-control-label {
cursor: not-allowed;
- color: #868686;
+ color: #89888d;
}
.gl-form-checkbox.custom-control .custom-control-input ~ .custom-control-label {
cursor: pointer;
@@ -447,7 +447,7 @@ input.btn-block[type="button"] {
.custom-control-input
~ .custom-control-label::before {
background-color: #fff;
- border-color: #868686;
+ border-color: #89888d;
}
.gl-form-checkbox.custom-control
.custom-control-input:checked
@@ -490,8 +490,8 @@ input.btn-block[type="button"] {
.gl-form-checkbox.custom-control
.custom-control-input:disabled
~ .custom-control-label::before {
- background-color: #f0f0f0;
- border-color: #dbdbdb;
+ background-color: #ececef;
+ border-color: #dcdcde;
pointer-events: auto;
}
.gl-form-checkbox.custom-control
@@ -500,8 +500,8 @@ input.btn-block[type="button"] {
.gl-form-checkbox.custom-control
.custom-control-input[type="checkbox"]:indeterminate:disabled
~ .custom-control-label::before {
- background-color: #dbdbdb;
- border-color: #dbdbdb;
+ background-color: #dcdcde;
+ border-color: #dcdcde;
}
.gl-form-checkbox.custom-control
.custom-control-input:checked:disabled
@@ -509,7 +509,7 @@ input.btn-block[type="button"] {
.gl-form-checkbox.custom-control
.custom-control-input[type="checkbox"]:indeterminate:disabled
~ .custom-control-label::after {
- background-color: #5e5e5e;
+ background-color: #626168;
}
.gl-button {
display: inline-flex;
@@ -526,9 +526,9 @@ input.btn-block[type="button"] {
padding-right: 0.75rem;
background-color: transparent;
line-height: 1rem;
- color: #303030;
+ color: #333238;
fill: currentColor;
- box-shadow: inset 0 0 0 1px #bfbfbf;
+ box-shadow: inset 0 0 0 1px #bfbfc3;
justify-content: center;
align-items: center;
font-size: 0.875rem;
@@ -560,9 +560,9 @@ input.btn-block[type="button"] {
.gl-button.gl-button.btn-default.active,
.gl-button.gl-button.btn-block.btn-default:active,
.gl-button.gl-button.btn-block.btn-default.active {
- box-shadow: inset 0 0 0 1px #5e5e5e, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
+ box-shadow: inset 0 0 0 1px #626168, 0 0 0 1px #fff, 0 0 0 3px #428fdc;
outline: none;
- background-color: #dbdbdb;
+ background-color: #dcdcde;
}
.gl-button.gl-button.btn-confirm,
.gl-button.gl-button.btn-block.btn-confirm {
@@ -636,20 +636,20 @@ body.navless {
font-weight: 400;
padding: 6px 10px;
background-color: #fff;
- border-color: #dbdbdb;
- color: #303030;
- color: #303030;
+ border-color: #dcdcde;
+ color: #333238;
+ color: #333238;
white-space: nowrap;
}
.btn:active {
- background-color: #f0f0f0;
+ background-color: #ececef;
box-shadow: none;
}
.btn:active,
.btn.active {
background-color: #eaeaea;
border-color: #e3e3e3;
- color: #303030;
+ color: #333238;
}
.btn svg {
height: 15px;
@@ -676,7 +676,7 @@ body.navless {
}
hr {
margin: 1.5rem 0;
- border-top: 1px solid #eee;
+ border-top: 1px solid #ececef;
}
.footer-links {
margin-bottom: 20px;
@@ -704,7 +704,7 @@ hr {
}
input {
border-radius: 0.25rem;
- color: #303030;
+ color: #333238;
background-color: #fff;
}
label {
@@ -721,7 +721,7 @@ label.label-bold {
padding: 6px 10px;
}
.form-control::placeholder {
- color: #868686;
+ color: #89888d;
}
.gl-show-field-errors .form-control:not(textarea) {
height: 34px;
@@ -730,7 +730,7 @@ label.label-bold {
justify-content: center;
height: var(--header-height, 48px);
background: #fff;
- border-bottom: 1px solid #dbdbdb;
+ border-bottom: 1px solid #dcdcde;
}
.navbar-empty .tanuki-logo,
.navbar-empty .brand-header-logo {
@@ -747,14 +747,14 @@ label.label-bold {
fill: #fca326;
}
input::-moz-placeholder {
- color: #868686;
+ color: #89888d;
opacity: 1;
}
input::-ms-input-placeholder {
- color: #868686;
+ color: #89888d;
}
input:-ms-input-placeholder {
- color: #868686;
+ color: #89888d;
}
svg {
fill: currentColor;
@@ -805,7 +805,7 @@ svg {
}
.login-page .login-box,
.login-page .omniauth-container {
- box-shadow: 0 0 0 1px #dbdbdb;
+ box-shadow: 0 0 0 1px #dcdcde;
border-radius: 0.25rem;
}
.login-page .login-box .login-heading h3,
@@ -863,7 +863,7 @@ svg {
}
.login-page .new-session-tabs {
display: flex;
- box-shadow: 0 0 0 1px #dbdbdb;
+ box-shadow: 0 0 0 1px #dcdcde;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
}
@@ -874,7 +874,7 @@ svg {
.login-page .new-session-tabs.nav-links-unboxed .nav-item {
border-left: 0;
border-right: 0;
- border-bottom: 1px solid #dbdbdb;
+ border-bottom: 1px solid #dcdcde;
background-color: transparent;
}
.login-page .new-session-tabs.custom-provider-tabs {
@@ -885,7 +885,7 @@ svg {
flex-basis: auto;
}
.login-page .new-session-tabs.custom-provider-tabs li:nth-child(n + 5) {
- border-top: 1px solid #dbdbdb;
+ border-top: 1px solid #dcdcde;
}
.login-page .new-session-tabs.custom-provider-tabs a {
font-size: 16px;
@@ -893,7 +893,7 @@ svg {
.login-page .new-session-tabs li {
flex: 1;
text-align: center;
- border-left: 1px solid #dbdbdb;
+ border-left: 1px solid #dcdcde;
}
.login-page .new-session-tabs li:first-of-type {
border-left: 0;
@@ -903,7 +903,7 @@ svg {
border-top-right-radius: 4px;
}
.login-page .new-session-tabs li:not(.active) {
- background-color: #fafafa;
+ background-color: #fbfafd;
}
.login-page .new-session-tabs li a {
width: 100%;
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 8e8cabbe511..a3474d2ed50 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -1,15 +1,15 @@
-$gray-10: #1f1f1f;
-$gray-50: #303030;
-$gray-100: #404040;
-$gray-200: #525252;
-$gray-300: #5e5e5e;
-$gray-400: #868686;
-$gray-500: #999;
-$gray-600: #bfbfbf;
-$gray-700: #dbdbdb;
-$gray-800: #f0f0f0;
-$gray-900: #fafafa;
-$gray-950: #fff;
+$gray-10: #1f1e24;
+$gray-50: #333238;
+$gray-100: #434248;
+$gray-200: #535158;
+$gray-300: #626168;
+$gray-400: #737278;
+$gray-500: #89888d;
+$gray-600: #a4a3a8;
+$gray-700: #bfbfc3;
+$gray-800: #dcdcde;
+$gray-900: #ececef;
+$gray-950: #fbfafd;
$green-50: #0a4020;
$green-100: #0d532a;
@@ -203,6 +203,7 @@ body.gl-dark {
--white: #{$white};
--black: #{$black};
+ --gray-light: #{$gray-50};
--svg-status-bg: #{$white};
@@ -255,9 +256,6 @@ $popover-arrow-outer-color: $gray-800;
$secondary: $gray-600;
-$issues-today-bg: #333838;
-$issues-today-border: #333a40;
-
$yiq-text-dark: $gray-50;
$yiq-text-light: $gray-950;
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index d644d8acc98..f37b426cd91 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -219,6 +219,16 @@
}
}
+ .search-sidebar {
+ .nav-link {
+ &.active,
+ &:hover {
+ background-color: rgba($gray-50, 0.8);
+ color: $gray-900;
+ }
+ }
+ }
+
// Sidebar
.nav-sidebar li.active > a {
color: $gray-900;