summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue112
-rw-r--r--app/assets/javascripts/admin/background_migrations/components/database_listbox.vue2
-rw-r--r--app/assets/javascripts/admin/users/components/actions/ban.vue4
-rw-r--r--app/assets/javascripts/alert_handler.js3
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue1
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue4
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue51
-rw-r--r--app/assets/javascripts/api/user_api.js2
-rw-r--r--app/assets/javascripts/artifacts/components/artifact_row.vue2
-rw-r--r--app/assets/javascripts/artifacts/components/feedback_banner.vue41
-rw-r--r--app/assets/javascripts/artifacts/components/job_artifacts_table.vue6
-rw-r--r--app/assets/javascripts/artifacts/constants.js7
-rw-r--r--app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql1
-rw-r--r--app/assets/javascripts/artifacts/index.js5
-rw-r--r--app/assets/javascripts/autosave.js59
-rw-r--r--app/assets/javascripts/batch_comments/components/submit_dropdown.vue3
-rw-r--r--app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js1
-rw-r--r--app/assets/javascripts/behaviors/markdown/init_gfm.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js4
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcut.vue3
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js90
-rw-r--r--app/assets/javascripts/blob/notebook/index.js6
-rw-r--r--app/assets/javascripts/blob/openapi/index.js4
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js24
-rw-r--r--app/assets/javascripts/blob/viewer/index.js4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js4
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue18
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue8
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue7
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue6
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue7
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue13
-rw-r--r--app/assets/javascripts/boards/index.js12
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js7
-rw-r--r--app/assets/javascripts/boards/stores/getters.js4
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js (renamed from app/assets/javascripts/ci_variable_list/ci_variable_list.js)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_admin_variables.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue)57
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_autocomplete_tokens.js (renamed from app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue)1
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue (renamed from app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js (renamed from app/assets/javascripts/ci_variable_list/constants.js)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/client/add_project_environment.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/client/add_project_environment.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_variables.query.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_environments.query.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/queries/project_environments.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_variables.query.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/queries/variables.query.graphql (renamed from app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql)2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/graphql/settings.js (renamed from app/assets/javascripts/ci_variable_list/graphql/settings.js)4
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/index.js (renamed from app/assets/javascripts/ci_variable_list/index.js)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js (renamed from app/assets/javascripts/ci_variable_list/native_form_variable_list.js)0
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/utils.js (renamed from app/assets/javascripts/ci_variable_list/utils.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue8
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue (renamed from app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue86
-rw-r--r--app/assets/javascripts/ci/pipeline_new/constants.js (renamed from app/assets/javascripts/pipeline_new/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql (renamed from app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql (renamed from app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js (renamed from app/assets/javascripts/pipeline_new/graphql/resolvers.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/index.js (renamed from app/assets/javascripts/pipeline_new/index.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js (renamed from app/assets/javascripts/pipeline_new/utils/filter_variables.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_new/utils/format_refs.js55
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue147
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue63
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue9
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue10
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue1
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/play_pipeline_schedule.mutation.graphql6
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js3
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_stats.vue4
-rw-r--r--app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql2
-rw-r--r--app/assets/javascripts/ci/runner/project_runners/index.js23
-rw-r--r--app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue19
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/index.js5
-rw-r--r--app/assets/javascripts/constants.js7
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue9
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue50
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue4
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue2
-rw-r--r--app/assets/javascripts/deprecated_notes.js28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue3
-rw-r--r--app/assets/javascripts/diff.js23
-rw-r--r--app/assets/javascripts/diffs/components/app.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue1
-rw-r--r--app/assets/javascripts/diffs/components/merge_conflict_warning.vue4
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue7
-rw-r--r--app/assets/javascripts/diffs/store/actions.js6
-rw-r--r--app/assets/javascripts/editor/constants.js12
-rw-r--r--app/assets/javascripts/editor/schema/ci.json27
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_job.vue24
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue26
-rw-r--r--app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue25
-rw-r--r--app/assets/javascripts/environments/environment_details/constants.js17
-rw-r--r--app/assets/javascripts/environments/environment_details/deployments_table.vue55
-rw-r--r--app/assets/javascripts/environments/environment_details/empty_state.vue34
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue169
-rw-r--r--app/assets/javascripts/environments/environment_details/pagination.vue74
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql23
-rw-r--r--app/assets/javascripts/environments/mount_show.js37
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue20
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue41
-rw-r--r--app/assets/javascripts/flash.js96
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue34
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue12
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue76
-rw-r--r--app/assets/javascripts/frequent_items/constants.js2
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js29
-rw-r--r--app/assets/javascripts/frequent_items/store/mutation_types.js3
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js16
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js23
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js41
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json3
-rw-r--r--app/assets/javascripts/groups_select.js122
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue4
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js9
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js4
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue33
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue35
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue60
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js8
-rw-r--r--app/assets/javascripts/init_diff_stats_dropdown.js2
-rw-r--r--app/assets/javascripts/integrations/constants.js4
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue78
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_forms/section.vue90
-rw-r--r--app/assets/javascripts/integrations/edit/components/sections/trigger.vue3
-rw-r--r--app/assets/javascripts/integrations/edit/components/trigger_field.vue29
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue7
-rw-r--r--app/assets/javascripts/invite_members/components/user_limit_notification.vue32
-rw-r--r--app/assets/javascripts/invite_members/constants.js12
-rw-r--r--app/assets/javascripts/issuable/components/issuable_header_warnings.vue11
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue1
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js21
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue116
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js12
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql12
-rw-r--r--app/assets/javascripts/issues/dashboard/utils.js23
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue1
-rw-r--r--app/assets/javascripts/issues/list/constants.js13
-rw-r--r--app/assets/javascripts/issues/list/utils.js12
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue22
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js13
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue70
-rw-r--r--app/assets/javascripts/issues/show/index.js6
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue87
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/app.vue6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue73
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue16
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue4
-rw-r--r--app/assets/javascripts/jobs/components/job/manual_variables_form.vue16
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue6
-rw-r--r--app/assets/javascripts/jobs/store/actions.js4
-rw-r--r--app/assets/javascripts/language_switcher/components/app.vue26
-rw-r--r--app/assets/javascripts/layout_nav.js77
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js12
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js23
-rw-r--r--app/assets/javascripts/lib/utils/poll_until_complete.js4
-rw-r--r--app/assets/javascripts/locale/ensure_single_line.cjs (renamed from app/assets/javascripts/locale/ensure_single_line.js)0
-rw-r--r--app/assets/javascripts/locale/index.js2
-rw-r--r--app/assets/javascripts/main.js10
-rw-r--r--app/assets/javascripts/members/components/action_buttons/access_request_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue1
-rw-r--r--app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue2
-rw-r--r--app/assets/javascripts/members/components/action_buttons/leave_button.vue40
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue1
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_member_button.vue31
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue95
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/constants.js22
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue36
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue86
-rw-r--r--app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue134
-rw-r--r--app/assets/javascripts/members/components/modals/leave_modal.vue63
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue48
-rw-r--r--app/assets/javascripts/members/components/table/created_at.vue8
-rw-r--r--app/assets/javascripts/members/components/table/member_action_buttons.vue11
-rw-r--r--app/assets/javascripts/members/components/table/member_activity.vue38
-rw-r--r--app/assets/javascripts/members/components/table/member_source.vue41
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue42
-rw-r--r--app/assets/javascripts/members/components/table/members_table_cell.vue7
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue40
-rw-r--r--app/assets/javascripts/members/constants.js17
-rw-r--r--app/assets/javascripts/members/guest_overage_confirm_action.js3
-rw-r--r--app/assets/javascripts/members/index.js6
-rw-r--r--app/assets/javascripts/members/utils.js6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js42
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue8
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue50
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue89
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js8
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js6
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js8
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue29
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue11
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js2
-rw-r--r--app/assets/javascripts/notifications/components/notification_email_listbox_input.vue46
-rw-r--r--app/assets/javascripts/notifications/index.js27
-rw-r--r--app/assets/javascripts/observability/components/observability_app.vue26
-rw-r--r--app/assets/javascripts/observability/components/skeleton/index.vue128
-rw-r--r--app/assets/javascripts/observability/constants.js30
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue51
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue12
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue29
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue56
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue8
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js60
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js7
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index/index.js9
-rw-r--r--app/assets/javascripts/pages/groups/boards/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/observability/datasources/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js2
-rw-r--r--app/assets/javascripts/pages/import/bitbucket/status/index.js21
-rw-r--r--app/assets/javascripts/pages/import/bitbucket_server/status/index.js30
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue65
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/status/index.js2
-rw-r--r--app/assets/javascripts/pages/import/gitea/status/index.js2
-rw-r--r--app/assets/javascripts/pages/import/github/status/index.js2
-rw-r--r--app/assets/javascripts/pages/import/gitlab/status/index.js2
-rw-r--r--app/assets/javascripts/pages/import/manifest/status/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue84
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue78
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue9
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/settings/merge_requests/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/usage_quotas/index.js9
-rw-r--r--app/assets/javascripts/pages/users/index.js2
-rw-r--r--app/assets/javascripts/pipeline_new/components/refs_dropdown.vue114
-rw-r--r--app/assets/javascripts/pipeline_new/utils/format_refs.js18
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/wrapper.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue9
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue44
-rw-r--r--app/assets/javascripts/pipelines/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js4
-rw-r--r--app/assets/javascripts/profile/preferences/profile_preferences_bundle.js3
-rw-r--r--app/assets/javascripts/profile/profile.js4
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue67
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue13
-rw-r--r--app/assets/javascripts/projects/commit/components/projects_dropdown.vue57
-rw-r--r--app/assets/javascripts/projects/commits/index.js19
-rw-r--r--app/assets/javascripts/projects/merge_requests/components/report_abuse_dropdown_item.vue41
-rw-r--r--app/assets/javascripts/projects/merge_requests/index.js18
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue8
-rw-r--r--app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js4
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue12
-rw-r--r--app/assets/javascripts/read_more.js4
-rw-r--r--app/assets/javascripts/ref/components/ref_results_section.vue8
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue15
-rw-r--r--app/assets/javascripts/ref/constants.js2
-rw-r--r--app/assets/javascripts/ref/stores/mutations.js4
-rw-r--r--app/assets/javascripts/ref_select_dropdown.js51
-rw-r--r--app/assets/javascripts/repository/commits_service.js2
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js2
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue31
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/openapi_viewer.vue24
-rw-r--r--app/assets/javascripts/repository/components/fork_info.vue146
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue17
-rw-r--r--app/assets/javascripts/repository/constants.js6
-rw-r--r--app/assets/javascripts/repository/index.js24
-rw-r--r--app/assets/javascripts/repository/queries/fork_details.query.graphql9
-rw-r--r--app/assets/javascripts/repository/utils/ref_switcher_utils.js8
-rw-r--r--app/assets/javascripts/search/index.js11
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/results_filters.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_navigation.vue17
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js13
-rw-r--r--app/assets/javascripts/search/sidebar/index.js6
-rw-r--r--app/assets/javascripts/search/store/constants.js2
-rw-r--r--app/assets/javascripts/search/store/utils.js24
-rw-r--r--app/assets/javascripts/search/topbar/constants.js2
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js6
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue39
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue27
-rw-r--r--app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue54
-rw-r--r--app/assets/javascripts/set_status_modal/utils.js10
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue5
-rw-r--r--app/assets/javascripts/super_sidebar/components/bottom_bar.vue24
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher.vue83
-rw-r--r--app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue45
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue48
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue37
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue50
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue77
-rw-r--r--app/assets/javascripts/super_sidebar/mock_data.js59
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js26
-rw-r--r--app/assets/javascripts/syntax_highlight.js2
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue6
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue134
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue142
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/storage_type_icon.vue35
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue170
-rw-r--r--app/assets/javascripts/usage_quotas/storage/constants.js100
-rw-r--r--app/assets/javascripts/usage_quotas/storage/init_project_storage.js34
-rw-r--r--app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql17
-rw-r--r--app/assets/javascripts/usage_quotas/storage/utils.js49
-rw-r--r--app/assets/javascripts/users/profile/components/report_abuse_button.vue55
-rw-r--r--app/assets/javascripts/users/profile/index.js18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue58
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue41
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue134
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/group_select.vue91
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js48
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js42
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue58
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue38
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/navigation_tabs.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js63
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/constants.js73
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql (renamed from app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql (renamed from app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue123
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue169
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue35
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue201
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue15
-rw-r--r--app/assets/javascripts/vue_shared/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue5
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/activity_filter.vue113
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note.vue59
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_body.vue37
-rw-r--r--app/assets/javascripts/work_items/components/work_item_comment_form.vue228
-rw-r--r--app/assets/javascripts/work_items/components/work_item_comment_locked.vue66
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue67
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue21
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue41
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue45
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue149
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue14
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_note_signed_out.vue31
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue158
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue5
-rw-r--r--app/assets/javascripts/work_items/constants.js31
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql5
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_links.query.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_note.fragment.graphql (renamed from app/assets/javascripts/work_items/graphql/discussion.fragment.graphql)8
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql6
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql2
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql2
-rw-r--r--app/assets/javascripts/work_items/index.js4
-rw-r--r--app/assets/stylesheets/fonts.scss29
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss7
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss12
-rw-r--r--app/assets/stylesheets/framework/selects.scss11
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss22
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/lazy_bundles/select2_overrides.scss16
-rw-r--r--app/assets/stylesheets/page_bundles/ci_status.scss3
-rw-r--r--app/assets/stylesheets/page_bundles/editor.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/import.scss45
-rw-r--r--app/assets/stylesheets/page_bundles/members.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss14
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/todos.scss4
-rw-r--r--app/assets/stylesheets/pages/ml_experiment_tracking.scss14
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss13
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss17
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss13
-rw-r--r--app/assets/stylesheets/themes/_dark.scss4
-rw-r--r--app/assets/stylesheets/utilities.scss43
466 files changed, 7613 insertions, 3179 deletions
diff --git a/app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue b/app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue
new file mode 100644
index 00000000000..c716afbbcf0
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports/components/abuse_category_selector.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlButton, GlDrawer, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
+import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import { s__, __ } from '~/locale';
+import csrf from '~/lib/utils/csrf';
+
+export default {
+ name: 'AbuseCategorySelector',
+ csrf,
+ components: {
+ GlButton,
+ GlDrawer,
+ GlForm,
+ GlFormGroup,
+ GlFormRadioGroup,
+ },
+ inject: {
+ reportAbusePath: {
+ default: '',
+ },
+ reportedUserId: {
+ default: '',
+ },
+ reportedFromUrl: {
+ default: '',
+ },
+ },
+ props: {
+ showDrawer: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ i18n: {
+ title: __('Report abuse to administrator'),
+ close: __('Close'),
+ label: s__('ReportAbuse|Why are you reporting this user?'),
+ next: __('Next'),
+ },
+ categoryOptions: [
+ { value: 'spam', text: s__("ReportAbuse|They're posting spam.") },
+ { value: 'offensive', text: s__("ReportAbuse|They're being offsensive or abusive.") },
+ { value: 'phishing', text: s__("ReportAbuse|They're phising.") },
+ { value: 'crypto', text: s__("ReportAbuse|They're crypto mining.") },
+ {
+ value: 'credentials',
+ text: s__("ReportAbuse|They're posting personal information or credentials."),
+ },
+ { value: 'copyright', text: s__("ReportAbuse|They're violating a copyright or trademark.") },
+ { value: 'malware', text: s__("ReportAbuse|They're posting malware.") },
+ { value: 'other', text: s__('ReportAbuse|Something else.') },
+ ],
+ data() {
+ return {
+ selected: '',
+ };
+ },
+ computed: {
+ drawerOffsetTop() {
+ return getContentWrapperHeight('.content-wrapper');
+ },
+ },
+ methods: {
+ closeDrawer() {
+ this.$emit('close-drawer');
+ },
+ },
+};
+</script>
+<template>
+ <gl-drawer
+ :header-height="drawerOffsetTop"
+ :z-index="300"
+ :open="showDrawer"
+ @close="closeDrawer"
+ >
+ <template #title>
+ <h2
+ class="gl-font-size-h2 gl-mt-0 gl-mb-0 gl-line-height-24"
+ data-testid="category-drawer-title"
+ >
+ {{ $options.i18n.title }}
+ </h2>
+ </template>
+ <template #default>
+ <gl-form :action="reportAbusePath" method="post" class="gl-text-left">
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+
+ <input type="hidden" name="user_id" :value="reportedUserId" data-testid="input-user-id" />
+ <input
+ type="hidden"
+ name="abuse_report[reported_from_url]"
+ :value="reportedFromUrl"
+ data-testid="input-referer"
+ />
+
+ <gl-form-group :label="$options.i18n.label">
+ <gl-form-radio-group
+ v-model="selected"
+ :options="$options.categoryOptions"
+ name="abuse_report[category]"
+ required
+ />
+ </gl-form-group>
+
+ <gl-button type="submit" variant="confirm" data-testid="submit-form-button">
+ {{ $options.i18n.next }}
+ </gl-button>
+ </gl-form>
+ </template>
+ </gl-drawer>
+</template>
diff --git a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
index 8e814cd55ef..7cc4a0d349d 100644
--- a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
+++ b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
@@ -35,7 +35,7 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-align-items-center" data-testid="database-listbox">
+ <div class="gl-display-flex gl-align-items-center">
<label id="label" class="gl-font-weight-bold gl-mr-4 gl-mb-0">{{
$options.i18n.database
}}</label>
diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue
index 55938832dce..898a688c203 100644
--- a/app/assets/javascripts/admin/users/components/actions/ban.vue
+++ b/app/assets/javascripts/admin/users/components/actions/ban.vue
@@ -11,7 +11,9 @@ const messageHtml = `
<ul>
<li>${s__("AdminUsers|The user can't log in.")}</li>
<li>${s__("AdminUsers|The user can't access git repositories.")}</li>
- <li>${s__('AdminUsers|Issues authored by this user are hidden from other users.')}</li>
+ <li>${s__(
+ 'AdminUsers|Issues and merge requests authored by this user are hidden from other users.',
+ )}</li>
</ul>
<p>${s__('AdminUsers|You can unban their account in the future. Their data remains intact.')}</p>
<p>${sprintf(
diff --git a/app/assets/javascripts/alert_handler.js b/app/assets/javascripts/alert_handler.js
index 3c867f196d6..9d53101fb22 100644
--- a/app/assets/javascripts/alert_handler.js
+++ b/app/assets/javascripts/alert_handler.js
@@ -2,10 +2,11 @@
// Note: This ONLY works on elements that are created on page load
// You can follow this effort in the following epic
// https://gitlab.com/groups/gitlab-org/-/epics/4070
+import { __ } from '~/locale';
export default function initAlertHandler() {
const DISMISSIBLE_SELECTORS = ['.gl-alert', '.gl-banner'];
- const DISMISS_LABEL = '[aria-label="Dismiss"]';
+ const DISMISS_LABEL = `[aria-label="${__('Dismiss')}"]`;
const DISMISS_CLASS = '.gl-alert-dismiss';
DISMISSIBLE_SELECTORS.forEach((selector) => {
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 65c3bc732ed..428291f2313 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -503,6 +503,7 @@ export default {
v-model="integrationForm.apiUrl"
type="text"
:placeholder="$options.placeholders.prometheus"
+ data-qa-selector="prometheus_url_field"
@input="validateApiUrl"
/>
<span class="gl-text-gray-400">
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 010cb5721a1..7dd33da435a 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -4,7 +4,7 @@ import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mu
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_FORBIDDEN } from '~/lib/utils/http_status';
import { typeSet, i18n, tabIndices } from '../constants';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
@@ -327,7 +327,7 @@ export default {
})
.catch((error) => {
let message = INTEGRATION_PAYLOAD_TEST_ERROR;
- if (error.response?.status === httpStatusCodes.FORBIDDEN) {
+ if (error.response?.status === HTTP_STATUS_FORBIDDEN) {
message = INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR;
}
createAlert({ message });
diff --git a/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue b/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue
deleted file mode 100644
index a5c20b237b3..00000000000
--- a/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { redirectTo } from '~/lib/utils/url_utility';
-import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
-
-export default {
- name: 'MetricTile',
- components: {
- GlSingleStat,
- MetricPopover,
- },
- props: {
- metric: {
- type: Object,
- required: true,
- },
- },
- computed: {
- decimalPlaces() {
- const parsedFloat = parseFloat(this.metric.value);
- return Number.isNaN(parsedFloat) || Number.isInteger(parsedFloat) ? 0 : 1;
- },
- hasLinks() {
- return this.metric.links?.length && this.metric.links[0].url;
- },
- },
- methods: {
- clickHandler({ links }) {
- if (this.hasLinks) {
- redirectTo(links[0].url);
- }
- },
- },
-};
-</script>
-<template>
- <div v-bind="$attrs">
- <gl-single-stat
- :id="metric.identifier"
- :value="`${metric.value}`"
- :title="metric.label"
- :unit="metric.unit || ''"
- :should-animate="true"
- :animation-decimal-places="decimalPlaces"
- :class="{ 'gl-hover-cursor-pointer': hasLinks }"
- tabindex="0"
- @click="clickHandler(metric)"
- />
- <metric-popover :metric="metric" :target="metric.identifier" />
- </div>
-</template>
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index 0f874e35684..45fddc3a696 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -65,7 +65,7 @@ export function getUserProjects(userId, query, options, callback) {
export function updateUserStatus({ emoji, message, availability, clearStatusAfter }) {
const url = buildApiUrl(USER_POST_STATUS_PATH);
- return axios.put(url, {
+ return axios.patch(url, {
emoji,
message,
availability,
diff --git a/app/assets/javascripts/artifacts/components/artifact_row.vue b/app/assets/javascripts/artifacts/components/artifact_row.vue
index 8c03db2acd1..fffdfce60a7 100644
--- a/app/assets/javascripts/artifacts/components/artifact_row.vue
+++ b/app/assets/javascripts/artifacts/components/artifact_row.vue
@@ -11,6 +11,7 @@ export default {
GlBadge,
GlFriendlyWrap,
},
+ inject: ['canDestroyArtifacts'],
props: {
artifact: {
type: Object,
@@ -73,6 +74,7 @@ export default {
data-testid="job-artifact-row-download-button"
/>
<gl-button
+ v-if="canDestroyArtifacts"
category="tertiary"
icon="remove"
:title="$options.i18n.delete"
diff --git a/app/assets/javascripts/artifacts/components/feedback_banner.vue b/app/assets/javascripts/artifacts/components/feedback_banner.vue
new file mode 100644
index 00000000000..d2c96b1a201
--- /dev/null
+++ b/app/assets/javascripts/artifacts/components/feedback_banner.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import {
+ I18N_FEEDBACK_BANNER_TITLE,
+ I18N_FEEDBACK_BANNER_BODY,
+ I18N_FEEDBACK_BANNER_BUTTON,
+ FEEDBACK_URL,
+} from '../constants';
+
+export default {
+ components: {
+ GlBanner,
+ UserCalloutDismisser,
+ },
+ inject: ['artifactsManagementFeedbackImagePath'],
+ FEEDBACK_URL,
+ i18n: {
+ title: I18N_FEEDBACK_BANNER_TITLE,
+ body: I18N_FEEDBACK_BANNER_BODY,
+ button: I18N_FEEDBACK_BANNER_BUTTON,
+ },
+};
+</script>
+<template>
+ <user-callout-dismisser feature-name="artifacts_management_page_feedback_banner">
+ <template #default="{ dismiss, shouldShowCallout }">
+ <gl-banner
+ v-if="shouldShowCallout"
+ class="gl-mb-6"
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.button"
+ :button-link="$options.FEEDBACK_URL"
+ :svg-path="artifactsManagementFeedbackImagePath"
+ @close="dismiss"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ </gl-banner>
+ </template>
+ </user-callout-dismisser>
+</template>
diff --git a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
index 34e443f4e58..5743ff3ec9e 100644
--- a/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/artifacts/components/job_artifacts_table.vue
@@ -35,6 +35,7 @@ import {
INITIAL_LAST_PAGE_SIZE,
} from '../constants';
import ArtifactsTableRowDetails from './artifacts_table_row_details.vue';
+import FeedbackBanner from './feedback_banner.vue';
const INITIAL_PAGINATION_STATE = {
currentPage: INITIAL_CURRENT_PAGE,
@@ -58,8 +59,9 @@ export default {
CiIcon,
TimeAgo,
ArtifactsTableRowDetails,
+ FeedbackBanner,
},
- inject: ['projectPath'],
+ inject: ['projectPath', 'canDestroyArtifacts'],
apollo: {
jobArtifacts: {
query: getJobArtifactsQuery,
@@ -214,6 +216,7 @@ export default {
</script>
<template>
<div>
+ <feedback-banner />
<gl-table
:items="jobArtifacts"
:fields="$options.fields"
@@ -308,6 +311,7 @@ export default {
data-testid="job-artifacts-browse-button"
/>
<gl-button
+ v-if="canDestroyArtifacts"
icon="remove"
:title="$options.i18n.delete"
:aria-label="$options.i18n.delete"
diff --git a/app/assets/javascripts/artifacts/constants.js b/app/assets/javascripts/artifacts/constants.js
index 5fcc4f2b76e..28fd81fa641 100644
--- a/app/assets/javascripts/artifacts/constants.js
+++ b/app/assets/javascripts/artifacts/constants.js
@@ -43,6 +43,13 @@ export const I18N_MODAL_BODY = s__(
export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
export const I18N_MODAL_CANCEL = __('Cancel');
+export const I18N_FEEDBACK_BANNER_TITLE = s__('Artifacts|Help us improve this page');
+export const I18N_FEEDBACK_BANNER_BODY = s__(
+ 'Artifacts|We want you to be able to use this page to easily manage your CI/CD job artifacts. We are working to improve this experience and would appreciate any feedback you have about the improvements we are making.',
+);
+export const I18N_FEEDBACK_BANNER_BUTTON = s__('Artifacts|Take a quick survey');
+export const FEEDBACK_URL = 'https://gitlab.fra1.qualtrics.com/jfe/form/SV_cI9rAUI20Vo2St8';
+
export const INITIAL_CURRENT_PAGE = 1;
export const INITIAL_PREVIOUS_PAGE_CURSOR = '';
export const INITIAL_NEXT_PAGE_CURSOR = '';
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
index 9777153999e..89a24d7891e 100644
--- a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
+++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
@@ -10,7 +10,6 @@ query getJobArtifacts(
project(fullPath: $projectPath) {
id
jobs(
- withArtifacts: true
statuses: [SUCCESS, FAILED]
first: $firstPageSize
last: $lastPageSize
diff --git a/app/assets/javascripts/artifacts/index.js b/app/assets/javascripts/artifacts/index.js
index b5146e0f0e9..e0b2ab2bf47 100644
--- a/app/assets/javascripts/artifacts/index.js
+++ b/app/assets/javascripts/artifacts/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import JobArtifactsTable from './components/job_artifacts_table.vue';
Vue.use(VueApollo);
@@ -16,13 +17,15 @@ export const initArtifactsTable = () => {
return false;
}
- const { projectPath } = el.dataset;
+ const { projectPath, canDestroyArtifacts, artifactsManagementFeedbackImagePath } = el.dataset;
return new Vue({
el,
apolloProvider,
provide: {
projectPath,
+ canDestroyArtifacts: parseBoolean(canDestroyArtifacts),
+ artifactsManagementFeedbackImagePath,
},
render: (createElement) => createElement(JobArtifactsTable),
});
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 5ab66acaf80..2e187eae17c 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,56 +1,57 @@
-/* eslint-disable no-param-reassign, consistent-return */
-
+import { parseBoolean } from '~/lib/utils/common_utils';
import AccessorUtilities from './lib/utils/accessor';
export default class Autosave {
constructor(field, key, fallbackKey, lockVersion) {
this.field = field;
- this.type = this.field.prop('type');
+ this.type = this.field.getAttribute('type');
this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
- if (key.join != null) {
- key = key.join('/');
- }
- this.key = `autosave/${key}`;
+ this.key = Array.isArray(key) ? `autosave/${key.join('/')}` : `autosave/${key}`;
this.fallbackKey = fallbackKey;
this.lockVersionKey = `${this.key}/lockVersion`;
this.lockVersion = lockVersion;
- this.field.data('autosave', this);
this.restore();
- this.field.on('input', () => this.save());
+ this.saveAction = this.save.bind(this);
+ // used by app/assets/javascripts/deprecated_notes.js
+ this.field.$autosave = this;
+ this.field.addEventListener('input', this.saveAction);
}
restore() {
if (!this.isLocalStorageAvailable) return;
- if (!this.field.length) return;
const text = window.localStorage.getItem(this.key);
const fallbackText = window.localStorage.getItem(this.fallbackKey);
+ const newValue = text || fallbackText;
+ if (newValue == null) return;
+
+ let originalValue = this.field.value;
if (this.type === 'checkbox') {
- this.field.prop('checked', text || fallbackText);
- } else if (text) {
- this.field.val(text);
- } else if (fallbackText) {
- this.field.val(fallbackText);
+ originalValue = this.field.checked;
+ this.field.checked = parseBoolean(newValue);
+ } else {
+ this.field.value = newValue;
}
- this.field.trigger('input');
- // v-model does not update with jQuery trigger
- // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
- const event = new Event('change', { bubbles: true, cancelable: false });
- const field = this.field.get(0);
- if (field) {
- field.dispatchEvent(event);
- }
+ if (originalValue === newValue) return;
+ this.triggerInputEvents();
+ }
+
+ triggerInputEvents() {
+ // trigger events so @input, @change and v-model trigger in Vue components
+ const inputEvent = new Event('input', { bubbles: true, cancelable: false });
+ const changeEvent = new Event('change', { bubbles: true, cancelable: false });
+ this.field.dispatchEvent(inputEvent);
+ this.field.dispatchEvent(changeEvent);
}
getSavedLockVersion() {
- if (!this.isLocalStorageAvailable) return;
+ if (!this.isLocalStorageAvailable) return undefined;
return window.localStorage.getItem(this.lockVersionKey);
}
save() {
- if (!this.field.length) return;
- const value = this.type === 'checkbox' ? this.field.is(':checked') : this.field.val();
+ const value = this.type === 'checkbox' ? this.field.checked : this.field.value;
if (this.isLocalStorageAvailable && value) {
if (this.fallbackKey) {
@@ -66,7 +67,7 @@ export default class Autosave {
}
reset() {
- if (!this.isLocalStorageAvailable) return;
+ if (!this.isLocalStorageAvailable) return undefined;
window.localStorage.removeItem(this.lockVersionKey);
window.localStorage.removeItem(this.fallbackKey);
@@ -74,7 +75,7 @@ export default class Autosave {
}
dispose() {
- // eslint-disable-next-line @gitlab/no-global-event-off
- this.field.off('input');
+ delete this.field.$autosave;
+ this.field.removeEventListener('input', this.saveAction);
}
}
diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
index acc3cbe10a0..ed0481e7a48 100644
--- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
+++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue
@@ -1,5 +1,4 @@
<script>
-import $ from 'jquery';
import {
GlDropdown,
GlButton,
@@ -52,7 +51,7 @@ export default {
},
mounted() {
this.autosave = new Autosave(
- $(this.$refs.textarea),
+ this.$refs.textarea,
`submit_review_dropdown/${this.getNoteableData.id}`,
);
this.noteData.noteable_type = this.noteableType;
diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
index d712c90242c..ff301a99243 100644
--- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
+++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js
@@ -11,6 +11,7 @@ export default function initGFMInput($els) {
emojis: true,
members: enableGFM,
issues: enableGFM,
+ iterations: enableGFM,
milestones: enableGFM,
mergeRequests: enableGFM,
labels: enableGFM,
diff --git a/app/assets/javascripts/behaviors/markdown/init_gfm.js b/app/assets/javascripts/behaviors/markdown/init_gfm.js
deleted file mode 100644
index d9c7cee50da..00000000000
--- a/app/assets/javascripts/behaviors/markdown/init_gfm.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import $ from 'jquery';
-import { renderGFM } from '~/behaviors/markdown/render_gfm';
-
-$.fn.renderGFM = function plugin() {
- this.get().forEach(renderGFM);
- return this;
-};
-requestIdleCallback(
- () => {
- renderGFM(document.body);
- },
- { timeout: 500 },
-);
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 2eab5b84e3e..04b3599ea8c 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -18,6 +18,10 @@ function initPopovers(elements) {
// Render GitLab flavoured Markdown
export function renderGFM(element) {
+ if (!element) {
+ return;
+ }
+
const [
highlightEls,
krokiEls,
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 86a05f24dfc..32e395e4f3c 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -1,10 +1,10 @@
/* eslint-disable func-names */
import $ from 'jquery';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
-import '~/behaviors/markdown/init_gfm';
// MarkdownPreview
//
@@ -51,7 +51,7 @@ MarkdownPreview.prototype.showPreview = function ($form) {
}
preview.removeClass('md-preview-loading').html(body);
- preview.renderGFM();
+ renderGFM(preview.get(0));
this.renderReferencedUsers(response.references.users, $form);
if (response.references.commands) {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcut.vue b/app/assets/javascripts/behaviors/shortcuts/shortcut.vue
index e5992779a99..38384157007 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcut.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcut.vue
@@ -1,4 +1,5 @@
<script>
+import { getModifierKey } from '~/constants';
import { __, s__ } from '~/locale';
// Map some keys to their proper representation depending on the system
@@ -22,7 +23,7 @@ const getKeyMap = () => {
keyMap.alt = keyMap.option;
// Mod is Command on Mac, and Ctrl on Windows/Linux
- keyMap.mod = window.gl?.client?.isMac ? keyMap.command : keyMap.ctrl;
+ keyMap.mod = getModifierKey(true);
return keyMap;
};
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index e0ef49b60d3..7bb6bc7e9bc 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -71,7 +71,7 @@ export default class ShortcutsNavigation extends Shortcuts {
iid: window.gl.mrWidgetData?.iid,
});
if (path) {
- visitUrl(path);
+ visitUrl(path, true);
}
}
}
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 361d736f740..4e47aa99fd8 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -66,7 +66,7 @@ export default {
return !this.hideDefaultActions;
},
isEmpty() {
- return this.blob.rawSize === 0;
+ return this.blob.rawSize === '0';
},
blobSwitcherDocIcon() {
return this.blob.richViewer?.fileType === 'csv' ? 'table' : 'document';
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index adc2649e5df..2ea3c93625d 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -11,7 +11,6 @@ import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
import LicenseSelector from './template_selectors/license_selector';
import MetricsDashboardSelector from './template_selectors/metrics_dashboard_selector';
-import FileTemplateTypeSelector from './template_selectors/type_selector';
export default class FileTemplateMediator {
constructor({ editor, currentAction, projectId }) {
@@ -20,7 +19,6 @@ export default class FileTemplateMediator {
this.projectId = projectId;
this.initTemplateSelectors();
- this.initTemplateTypeSelector();
this.initDomElements();
this.initDropdowns();
this.initPageEvents();
@@ -38,26 +36,6 @@ export default class FileTemplateMediator {
].map((TemplateSelectorClass) => new TemplateSelectorClass({ mediator: this }));
}
- initTemplateTypeSelector() {
- this.typeSelector = new FileTemplateTypeSelector({
- mediator: this,
- dropdownData: this.templateSelectors
- .map((templateSelector) => {
- const cfg = templateSelector.config;
-
- return {
- name: cfg.name,
- key: cfg.key,
- id: cfg.key,
- };
- })
- .reduce(
- (acc, current) => (acc.find((item) => item.id === current.id) ? acc : [...acc, current]),
- [],
- ),
- });
- }
-
initDomElements() {
const $templatesMenu = $('.template-selectors-menu');
const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu');
@@ -71,13 +49,10 @@ export default class FileTemplateMediator {
this.$fileContent = $fileEditor.find('#file-content');
this.$commitForm = $fileEditor.find('form');
this.$navLinks = $fileEditor.find('.nav-links');
- this.$templateTypes = this.$templateSelectors.find('.template-type-selector');
}
initDropdowns() {
- if (this.currentAction === 'create') {
- this.typeSelector.show();
- } else {
+ if (this.currentAction !== 'create') {
this.hideTemplateSelectorMenu();
}
@@ -101,32 +76,12 @@ export default class FileTemplateMediator {
const hash = urlPieces[1];
if (hash === 'preview') {
this.hideTemplateSelectorMenu();
- } else if (hash === 'editor' && !this.typeSelector.isHidden()) {
+ } else if (hash === 'editor' && this.templateSelectors.find((sel) => sel.dropdown !== null)) {
this.showTemplateSelectorMenu();
}
});
}
- selectTemplateType(item, e) {
- if (e) {
- e.preventDefault();
- }
-
- this.templateSelectors.forEach((selector) => {
- if (selector.config.key === item.key) {
- selector.show();
- } else {
- selector.hide();
- }
- });
- this.setTypeSelectorToggleText(item.name);
- this.cacheToggleText();
- }
-
- selectTemplateTypeOptions(options) {
- this.selectTemplateType(options.selectedObj, options.e);
- }
-
selectTemplateFile(selector, query, data) {
const self = this;
const { name } = selector.config;
@@ -139,7 +94,7 @@ export default class FileTemplateMediator {
this.setEditorContent(file);
this.setFilename(name);
selector.renderLoaded();
- this.typeSelector.setToggleText(name);
+
toast(__(`${query} template applied`), {
action: {
text: __('Undo'),
@@ -163,15 +118,20 @@ export default class FileTemplateMediator {
displayMatchedTemplateSelector() {
const currentInput = this.getFilename();
- this.templateSelectors.forEach((selector) => {
- const match = selector.config.pattern.test(currentInput);
-
- if (match) {
- this.typeSelector.show();
- this.selectTemplateType(selector.config);
- this.showTemplateSelectorMenu();
+ const matchedSelector = this.templateSelectors.find((sel) =>
+ sel.config.pattern.test(currentInput),
+ );
+ const currentSelector = this.templateSelectors.find((sel) => !sel.isHidden());
+
+ if (matchedSelector) {
+ if (currentSelector) {
+ currentSelector.hide();
}
- });
+ matchedSelector.show();
+ this.showTemplateSelectorMenu();
+ } else {
+ this.hideTemplateSelectorMenu();
+ }
}
fetchFileTemplate(type, query, data = {}) {
@@ -194,16 +154,13 @@ export default class FileTemplateMediator {
this.editor.navigateFileStart();
}
- findTemplateSelectorByKey(key) {
- return this.templateSelectors.find((selector) => selector.config.key === key);
- }
-
hideTemplateSelectorMenu() {
this.$templatesMenu.hide();
}
showTemplateSelectorMenu() {
this.$templatesMenu.show();
+ this.cacheToggleText();
}
cacheToggleText() {
@@ -219,7 +176,6 @@ export default class FileTemplateMediator {
this.setEditorContent(this.cachedContent);
this.setFilename(this.cachedFilename);
this.setTemplateSelectorToggleText();
- this.setTypeSelectorToggleText(__('Select a template type'));
}
getTemplateSelectorToggleText() {
@@ -234,14 +190,6 @@ export default class FileTemplateMediator {
.text(this.cachedToggleText);
}
- getTypeSelectorToggleText() {
- return this.typeSelector.getToggleText();
- }
-
- setTypeSelectorToggleText(text) {
- this.typeSelector.setToggleText(text);
- }
-
getFilename() {
return this.$filenameInput.val();
}
@@ -253,8 +201,4 @@ export default class FileTemplateMediator {
input.dispatchEvent(new Event('change'));
}
}
-
- getSelected() {
- return this.templateSelectors.find((selector) => selector.selected);
- }
}
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index 25fe29c4fbe..9259827edf1 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -1,13 +1,11 @@
import Vue from 'vue';
import NotebookViewer from './notebook_viewer.vue';
-export default () => {
- const el = document.getElementById('js-notebook-viewer');
-
+export default ({ el = document.getElementById('js-notebook-viewer'), relativeRawPath }) => {
return new Vue({
el,
provide: {
- relativeRawPath: el.dataset.relativeRawPath,
+ relativeRawPath: relativeRawPath || el.dataset.relativeRawPath,
},
render(createElement) {
return createElement(NotebookViewer, {
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 8cfdc00bb40..2386508aef5 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -15,8 +15,8 @@ const createSandbox = () => {
return iframeEl;
};
-export default async () => {
- const wrapperEl = document.getElementById('js-openapi-viewer');
+export default async (el = document.getElementById('js-openapi-viewer')) => {
+ const wrapperEl = el;
const sandboxEl = createSandbox();
const { data } = await axios.get(wrapperEl.dataset.endpoint);
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
deleted file mode 100644
index 65e7ff0594c..00000000000
--- a/app/assets/javascripts/blob/template_selectors/type_selector.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import FileTemplateSelector from '../file_template_selector';
-
-export default class FileTemplateTypeSelector extends FileTemplateSelector {
- constructor({ mediator, dropdownData }) {
- super(mediator);
- this.mediator = mediator;
- this.config = {
- dropdown: '.js-template-type-selector',
- wrapper: '.js-template-type-selector-wrap',
- dropdownData,
- };
- }
-
- initDropdown() {
- initDeprecatedJQueryDropdown(this.$dropdown, {
- data: this.config.dropdownData,
- filterable: false,
- selectable: true,
- clicked: (options) => this.mediator.selectTemplateTypeOptions(options),
- text: (item) => item.name,
- });
- }
-}
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 439c4258805..5e85e4cea38 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '~/behaviors/markdown/init_gfm';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
@@ -195,7 +195,7 @@ export class BlobViewer {
this.toggleCopyButtonState();
loadViewer(newViewer)
.then((viewer) => {
- $(viewer).renderGFM();
+ renderGFM(viewer);
window.requestIdleCallback(() => {
this.$fileHolder.trigger('highlight:line');
handleLocationHash();
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 46b3f16df77..a3d11d90ed2 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
@@ -9,7 +10,6 @@ import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
-import '~/behaviors/markdown/init_gfm';
export default class EditBlob {
// The options object has:
@@ -140,7 +140,7 @@ export default class EditBlob {
})
.then(({ data }) => {
currentPane.empty().append(data);
- currentPane.renderGFM();
+ renderGFM(currentPane.get(0));
})
.catch(() =>
createAlert({
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 1335a3b108b..970e3509d20 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', 'fullBoardId'],
+ inject: ['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-id="fullBoardId" />
+ <board-content :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 f3307977be9..0c64cbad5b1 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -9,6 +9,7 @@ export default {
BoardCardInner,
},
mixins: [Tracking.mixin()],
+ inject: ['disabled'],
props: {
list: {
type: Object,
@@ -20,11 +21,6 @@ export default {
default: () => ({}),
required: false,
},
- disabled: {
- type: Boolean,
- default: false,
- required: false,
- },
index: {
type: Number,
default: 0,
@@ -35,6 +31,11 @@ export default {
default: false,
required: false,
},
+ canAdmin: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
...mapState(['selectedBoardItems', 'activeId']),
@@ -48,10 +49,10 @@ export default {
);
},
isDisabled() {
- return this.disabled || !this.item.id || this.item.isLoading;
+ return this.disabled || !this.item.id || this.item.isLoading || !this.canAdmin;
},
isDraggable() {
- return !this.disabled && this.item.id && !this.item.isLoading;
+ return !this.isDisabled;
},
cardStyle() {
return this.isColorful && this.item.color ? { borderColor: this.item.color } : '';
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 05c786ca61d..77df111afc1 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -8,7 +8,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { sortBy } from 'lodash';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
@@ -43,7 +43,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [boardCardInner],
- inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard'],
+ inject: ['rootPath', 'scopedLabelsAvailable', 'isEpicBoard', 'issuableType', 'isGroupBoard'],
props: {
item: {
type: Object,
@@ -77,8 +77,7 @@ export default {
};
},
computed: {
- ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
- ...mapGetters(['isProjectBoard']),
+ ...mapState(['isShowingLabels', 'allowSubEpics']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
@@ -158,7 +157,7 @@ export default {
return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100);
},
showReferencePath() {
- return !this.isProjectBoard && this.itemReferencePath;
+ return this.isGroupBoard && this.itemReferencePath;
},
avatarSize() {
return { default: 16, lg: 24 };
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 8fc76c02e14..b728b8dd22a 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -20,10 +20,6 @@ export default {
default: () => ({}),
required: false,
},
- disabled: {
- type: Boolean,
- required: true,
- },
},
computed: {
...mapState(['filterParams', 'highlightedLists']),
@@ -87,8 +83,8 @@ export default {
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
:class="{ 'board-column-highlighted': highlighted }"
>
- <board-list-header :list="list" :disabled="disabled" />
- <board-list ref="board-list" :disabled="disabled" :board-items="listItems" :list="list" />
+ <board-list-header :list="list" />
+ <board-list ref="board-list" :board-items="listItems" :list="list" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index ca86894ca40..92f79e61f14 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -9,7 +9,7 @@ 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, BoardType, listsQuery } from 'ee_else_ce/boards/constants';
+import { DraggableItemTypes, listsQuery } from 'ee_else_ce/boards/constants';
import BoardColumn from './board_column.vue';
export default {
@@ -35,13 +35,11 @@ export default {
'issuableType',
'isIssueBoard',
'isEpicBoard',
+ 'isGroupBoard',
+ 'disabled',
'isApolloBoard',
],
props: {
- disabled: {
- type: Boolean,
- required: true,
- },
boardId: {
type: String,
required: true,
@@ -89,8 +87,8 @@ export default {
queryVariables() {
return {
...(this.isIssueBoard && {
- isGroup: this.boardType === BoardType.group,
- isProject: this.boardType === BoardType.project,
+ isGroup: this.isGroupBoard,
+ isProject: !this.isGroupBoard,
}),
fullPath: this.fullPath,
boardId: this.boardId,
@@ -176,7 +174,6 @@ export default {
ref="board"
:list="list"
:data-draggable-item-type="$options.draggableItemTypes.list"
- :disabled="disabled"
:class="{ 'gl-xs-display-none!': addColumnFormVisible }"
/>
@@ -190,7 +187,6 @@ export default {
ref="swimlanes"
:lists="boardListsToUse"
:can-admin-list="canAdminList"
- :disabled="disabled"
:style="{ height: boardHeight }"
/>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 392a73b5859..e6d1e558c37 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -6,7 +6,7 @@ import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdow
import { __, sprintf } from '~/locale';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
-import { ISSUABLE, INCIDENT } from '~/boards/constants';
+import { BoardType, ISSUABLE, INCIDENT, issuableTypes } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
@@ -65,17 +65,22 @@ export default {
canUpdate: {
default: false,
},
+ issuableType: {
+ default: issuableTypes.issue,
+ },
+ isGroupBoard: {
+ default: false,
+ },
},
inheritAttrs: false,
computed: {
...mapGetters([
- 'isGroupBoard',
'isSidebarOpen',
'activeBoardItem',
'groupPathForActiveIssue',
'projectPathForActiveIssue',
]),
- ...mapState(['sidebarType', 'issuableType']),
+ ...mapState(['sidebarType']),
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
@@ -91,14 +96,17 @@ export default {
fullPath() {
return this.activeBoardItem?.referencePath?.split('#')[0] || '';
},
+ parentType() {
+ return this.isGroupBoard ? BoardType.group : BoardType.project;
+ },
createLabelTitle() {
return sprintf(__('Create %{workspace} label'), {
- workspace: this.isGroupBoard ? 'group' : 'project',
+ workspace: this.parentType,
});
},
manageLabelTitle() {
return sprintf(__('Manage %{workspace} labels'), {
- workspace: this.isGroupBoard ? 'group' : 'project',
+ workspace: this.parentType,
});
},
attrWorkspacePath() {
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 97f52f21e7f..ce86a4d3123 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -244,6 +244,13 @@ export default {
});
}
+ if (this.filterParams['not[healthStatus]']) {
+ filteredSearchValue.push({
+ type: TOKEN_TYPE_HEALTH,
+ value: { data: this.filterParams['not[healthStatus]'], operator: '!=' },
+ });
+ }
+
if (search) {
filteredSearchValue.push(search);
}
@@ -285,6 +292,7 @@ export default {
'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji,
'not[iteration_id]': this.filterParams.not.iterationId,
'not[release_tag]': this.filterParams.not.releaseTag,
+ 'not[health_status]': this.filterParams.not.healthStatus,
},
undefined,
);
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index fcf026bbe00..a71bde54a8f 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
-import { mapGetters, mapActions, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -51,6 +51,12 @@ export default {
boardBaseUrl: {
default: '',
},
+ isGroupBoard: {
+ default: false,
+ },
+ isProjectBoard: {
+ default: false,
+ },
},
props: {
canAdminBoard: {
@@ -84,7 +90,6 @@ export default {
},
computed: {
...mapState(['error']),
- ...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 215691c7ba2..060a708a22f 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -31,12 +31,8 @@ export default {
BoardCardMoveToPosition,
},
mixins: [Tracking.mixin()],
- inject: ['isEpicBoard'],
+ inject: ['isEpicBoard', 'disabled'],
props: {
- disabled: {
- type: Boolean,
- required: true,
- },
list: {
type: Object,
required: true,
@@ -314,7 +310,6 @@ export default {
:list="list"
:item="item"
: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 -->
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bfc4b52baaf..14dff8de70f 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -60,6 +60,9 @@ export default {
isEpicBoard: {
default: false,
},
+ disabled: {
+ default: true,
+ },
},
props: {
list: {
@@ -67,10 +70,6 @@ export default {
default: () => ({}),
required: false,
},
- disabled: {
- type: Boolean,
- required: true,
- },
isSwimlanesHeader: {
type: Boolean,
required: false,
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index 8db366e4995..8b9fafca306 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -16,7 +16,7 @@ export default {
ProjectSelect,
},
mixins: [BoardNewIssueMixin],
- inject: ['groupId'],
+ inject: ['groupId', 'fullPath', 'isGroupBoard'],
props: {
list: {
type: Object,
@@ -24,8 +24,8 @@ export default {
},
},
computed: {
- ...mapState(['selectedProject', 'fullPath']),
- ...mapGetters(['isGroupBoard', 'getBoardItemsByList']),
+ ...mapState(['selectedProject']),
+ ...mapGetters(['getBoardItemsByList']),
formEventPrefix() {
return toggleFormEventPrefix.issue;
},
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 4f90d77c0be..d26aeb69dd5 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -9,7 +9,7 @@ import {
GlModalDirective,
} from '@gitlab/ui';
import { throttle } from 'lodash';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { mapActions, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
@@ -49,6 +49,8 @@ export default {
'hasMissingBoards',
'scopedIssueBoardFeatureEnabled',
'weights',
+ 'boardType',
+ 'isGroupBoard',
],
props: {
throttleDuration: {
@@ -74,8 +76,7 @@ export default {
},
computed: {
- ...mapState(['boardType', 'board', 'isBoardLoading']),
- ...mapGetters(['isGroupBoard', 'isProjectBoard']),
+ ...mapState(['board', 'isBoardLoading']),
parentType() {
return this.boardType;
},
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 bc68c2e0e99..38a171e8889 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -4,7 +4,6 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { mapActions } from 'vuex';
import { orderBy } from 'lodash';
import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue';
-import { BoardType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import issueBoardFilters from '~/boards/issue_board_filters';
@@ -47,23 +46,15 @@ export default {
issue: __('Issue'),
},
components: { BoardFilteredSearch },
- inject: ['isSignedIn', 'releasesFetchPath', 'fullPath', 'boardType'],
+ inject: ['isSignedIn', 'releasesFetchPath', 'fullPath', 'isGroupBoard'],
computed: {
- isGroupBoard() {
- return this.boardType === BoardType.group;
- },
- epicsGroupPath() {
- return this.isGroupBoard
- ? this.fullPath
- : this.fullPath.slice(0, this.fullPath.lastIndexOf('/'));
- },
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
const { fetchUsers, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
- this.boardType,
+ this.isGroupBoard,
);
const tokens = [
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index f8bd81e6b98..968832a092d 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import BoardApp from '~/boards/components/board_app.vue';
import '~/boards/filters/due_date_filters';
-import { issuableTypes } from '~/boards/constants';
+import { BoardType, issuableTypes } from '~/boards/constants';
import store from '~/boards/stores';
import {
NavigationType,
@@ -31,17 +31,19 @@ function mountBoardApp(el) {
...convertObjectPropsToCamelCase(rawFilterParams),
};
+ const boardType = el.dataset.parent;
+
store.dispatch('fetchBoard', {
fullPath,
fullBoardId: fullBoardId(boardId),
- boardType: el.dataset.parent,
+ boardType,
});
store.dispatch('setInitialBoardData', {
boardId,
fullBoardId: fullBoardId(boardId),
fullPath,
- boardType: el.dataset.parent,
+ boardType,
disabled: parseBoolean(el.dataset.disabled) || true,
issuableType: issuableTypes.issue,
});
@@ -61,7 +63,9 @@ function mountBoardApp(el) {
fullPath,
initialFilterParams,
boardBaseUrl: el.dataset.boardBaseUrl,
- boardType: el.dataset.parent,
+ boardType,
+ isGroupBoard: boardType === BoardType.group,
+ isProjectBoard: boardType === BoardType.project,
currentUserId: gon.current_user_id || null,
boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
labelsManagePath: el.dataset.labelsManagePath,
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index 4bfd92fb748..7e9b68778d5 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -1,11 +1,8 @@
import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql';
import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql';
-import { BoardType } from './constants';
import boardLabels from './graphql/board_labels.query.graphql';
-export default function issueBoardFilters(apollo, fullPath, boardType) {
- const isGroupBoard = boardType === BoardType.group;
- const isProjectBoard = boardType === BoardType.project;
+export default function issueBoardFilters(apollo, fullPath, isGroupBoard) {
const transformLabels = ({ data }) => {
return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
};
@@ -34,7 +31,7 @@ export default function issueBoardFilters(apollo, fullPath, boardType) {
fullPath,
searchTerm: labelSearchTerm,
isGroup: isGroupBoard,
- isProject: isProjectBoard,
+ isProject: !isGroupBoard,
},
})
.then(transformLabels);
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index e1891a4d954..9e746f1a1b8 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -1,9 +1,7 @@
import { find } from 'lodash';
-import { BoardType, inactiveId, issuableTypes } from '../constants';
+import { inactiveId, issuableTypes } from '../constants';
export default {
- isGroupBoard: (state) => state.boardType === BoardType.group,
- isProjectBoard: (state) => state.boardType === BoardType.project,
isSidebarOpen: (state) => state.activeId !== inactiveId,
isSwimlanesOn: () => false,
getBoardItemById: (state) => (id) => {
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js
index 574a5e7fd99..574a5e7fd99 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci/ci_variable_list/ci_variable_list.js
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_admin_variables.vue
index 719696f682e..719696f682e 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_admin_variables.vue
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
index c9002edc1ab..7387a490177 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
@@ -1,15 +1,14 @@
<script>
-import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
+import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertEnvironmentScope } from '../utils';
export default {
name: 'CiEnvironmentsDropdown',
components: {
- GlDropdown,
- GlDropdownItem,
GlDropdownDivider,
- GlSearchBoxByType,
+ GlDropdownItem,
+ GlCollapsibleListbox,
},
props: {
environments: {
@@ -24,6 +23,7 @@ export default {
},
data() {
return {
+ selectedEnvironment: '',
searchTerm: '',
};
},
@@ -33,9 +33,15 @@ export default {
},
filteredEnvironments() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.environments.filter((environment) => {
- return environment.toLowerCase().includes(lowerCasedSearchTerm);
- });
+
+ return this.environments
+ .filter((environment) => {
+ return environment.toLowerCase().includes(lowerCasedSearchTerm);
+ })
+ .map((environment) => ({
+ value: environment,
+ text: environment,
+ }));
},
shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm);
@@ -47,44 +53,29 @@ export default {
methods: {
selectEnvironment(selected) {
this.$emit('select-environment', selected);
- this.clearSearch();
- },
- convertEnvironmentScopeValue(scope) {
- return convertEnvironmentScope(scope);
+ this.selectedEnvironment = selected;
},
createEnvironmentScope() {
this.$emit('create-environment-scope', this.searchTerm);
this.selectEnvironment(this.searchTerm);
},
- isSelected(env) {
- return this.selectedEnvironmentScope === env;
- },
- clearSearch() {
- this.searchTerm = '';
- },
},
};
</script>
<template>
- <gl-dropdown :text="environmentScopeLabel" @show="clearSearch">
- <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
- <gl-dropdown-item
- v-for="environment in filteredEnvironments"
- :key="environment"
- :is-checked="isSelected(environment)"
- is-check-item
- @click="selectEnvironment(environment)"
- >
- {{ convertEnvironmentScopeValue(environment) }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="!filteredEnvironments.length" ref="noMatchingResults">{{
- __('No matching results')
- }}</gl-dropdown-item>
- <template v-if="shouldRenderCreateButton">
+ <gl-collapsible-listbox
+ v-model="selectedEnvironment"
+ searchable
+ :items="filteredEnvironments"
+ :toggle-text="environmentScopeLabel"
+ @search="searchTerm = $event.trim()"
+ @select="selectEnvironment"
+ >
+ <template v-if="shouldRenderCreateButton" #footer>
<gl-dropdown-divider />
<gl-dropdown-item data-testid="create-wildcard-button" @click="createEnvironmentScope">
{{ composedCreateButtonLabel }}
</gl-dropdown-item>
</template>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
index 4466a6a8081..4466a6a8081 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue
index 6326940148a..6326940148a 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_autocomplete_tokens.js
index 3f25e3df305..3f25e3df305 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_autocomplete_tokens.js
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_autocomplete_tokens.js
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
index 00177539cdc..967125c7b0a 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue
@@ -352,7 +352,6 @@ export default {
</template>
<ci-environments-dropdown
v-if="areScopedVariablesAvailable"
- class="gl-w-full"
:selected-environment-scope="variable.environmentScope"
:environments="joinedEnvironments"
@select-environment="setEnvironmentScope"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
index 3c6114b38ce..3c6114b38ce 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index 6e39bda0b07..6e39bda0b07 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index 345a8def49d..345a8def49d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index 828d0724d93..828d0724d93 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
diff --git a/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
index a28ca4eebc9..a28ca4eebc9 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
index 9208c34f154..d6f3ddf086f 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addAdminVariable($variable: CiVariable!, $endpoint: String!) {
ciVariableMutation: addAdminVariable(variable: $variable, endpoint: $endpoint) @client {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
index a79b98f5e95..c00c8fb2a26 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteAdminVariable($variable: CiVariable!, $endpoint: String!) {
ciVariableMutation: deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
index ddea753bf90..d7b7cb77291 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateAdminVariable($variable: CiVariable!, $endpoint: String!) {
ciVariableMutation: updateAdminVariable(variable: $variable, endpoint: $endpoint) @client {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/client/add_project_environment.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/client/add_project_environment.mutation.graphql
index 45109762e80..45109762e80 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/client/add_project_environment.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/client/add_project_environment.mutation.graphql
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
index 24388637672..0dbb6c891fd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: addGroupVariable(
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
index f7c8e209ccd..b5d007237c8 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: deleteGroupVariable(
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
index 757e61a5cd3..4ffc091b490 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: updateGroupVariable(
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
index fa315084d86..67a02be3dc1 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addProjectVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: addProjectVariable(
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
index c3358cc35b9..4420404a7b4 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteProjectVariable(
$variable: CiVariable!
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
index fde92cef4cb..107746a19e9 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateProjectVariable(
$variable: CiVariable!
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_variables.query.graphql
index 900154cd24d..538502fdd3b 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/project_environments.query.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_environments.query.graphql
index 921e0ca25b9..921e0ca25b9 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/project_environments.query.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_environments.query.graphql
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_variables.query.graphql
index ee75eba7547..af0cd2d0b2c 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_variables.query.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/variables.query.graphql
index 9b255c3c182..b8dd6f5f562 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/queries/variables.query.graphql
@@ -1,4 +1,4 @@
-#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/ci/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
query getVariables($after: String, $first: Int = 100) {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/settings.js b/app/assets/javascripts/ci/ci_variable_list/graphql/settings.js
index 02f6c226b0f..10203383ba0 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/settings.js
+++ b/app/assets/javascripts/ci/ci_variable_list/graphql/settings.js
@@ -2,8 +2,8 @@ import axios from 'axios';
import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
-} from '../../lib/utils/common_utils';
-import { convertToGraphQLId, getIdFromGraphQLId } from '../../graphql_shared/utils';
+} from '~/lib/utils/common_utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
GRAPHQL_GROUP_TYPE,
GRAPHQL_PROJECT_TYPE,
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci/ci_variable_list/index.js
index 174a59aba42..174a59aba42 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci/ci_variable_list/index.js
diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js
index fdbefd8c313..fdbefd8c313 100644
--- a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
+++ b/app/assets/javascripts/ci/ci_variable_list/native_form_variable_list.js
diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_variable_list/utils.js
index eeca69274ce..eeca69274ce 100644
--- a/app/assets/javascripts/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_variable_list/utils.js
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
index 255e3cb31f1..891c40482d3 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
@@ -2,7 +2,6 @@
import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { SOURCE_EDITOR_DEBOUNCE } from '../../constants';
export default {
@@ -15,7 +14,6 @@ export default {
components: {
SourceEditor,
},
- mixins: [glFeatureFlagMixin()],
inject: ['ciConfigPath'],
inheritAttrs: false,
methods: {
@@ -23,10 +21,8 @@ export default {
this.$emit('updateCiConfig', content);
},
registerCiSchema({ detail: { instance } }) {
- if (this.glFeatures.schemaLinting) {
- instance.use({ definition: CiSchemaExtension });
- instance.registerCiSchema();
- }
+ instance.use({ definition: CiSchemaExtension });
+ instance.registerCiSchema();
},
},
readyEvent: EDITOR_READY_EVENT,
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
index 5692627abef..5692627abef 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue
diff --git a/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue b/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue
new file mode 100644
index 00000000000..060527f2662
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/components/refs_dropdown.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { DEBOUNCE_REFS_SEARCH_MS } from '../constants';
+import { formatListBoxItems, searchByFullNameInListboxOptions } from '../utils/format_refs';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+ inject: ['projectRefsEndpoint'],
+ props: {
+ value: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ isLoading: false,
+ searchTerm: '',
+ listBoxItems: [],
+ };
+ },
+ computed: {
+ lowerCasedSearchTerm() {
+ return this.searchTerm.toLowerCase();
+ },
+ refShortName() {
+ return this.value.shortName;
+ },
+ },
+ methods: {
+ loadRefs() {
+ this.isLoading = true;
+
+ axios
+ .get(this.projectRefsEndpoint, {
+ params: {
+ search: this.lowerCasedSearchTerm,
+ },
+ })
+ .then(({ data }) => {
+ // Note: These keys are uppercase in API
+ const { Branches = [], Tags = [] } = data;
+
+ this.listBoxItems = formatListBoxItems(Branches, Tags);
+ })
+ .catch((e) => {
+ this.$emit('loadingError', e);
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ debouncedLoadRefs: debounce(function debouncedLoadRefs() {
+ this.loadRefs();
+ }, DEBOUNCE_REFS_SEARCH_MS),
+ setRefSelected(refFullName) {
+ const ref = searchByFullNameInListboxOptions(refFullName, this.listBoxItems);
+ this.$emit('input', ref);
+ },
+ setSearchTerm(searchQuery) {
+ this.searchTerm = searchQuery?.trim();
+ this.debouncedLoadRefs();
+ },
+ },
+};
+</script>
+<template>
+ <gl-collapsible-listbox
+ class="gl-w-full gl-font-monospace"
+ :items="listBoxItems"
+ :searchable="true"
+ :searching="isLoading"
+ :search-placeholder="__('Search refs')"
+ :selected="value.fullName"
+ toggle-class="gl-flex-direction-column gl-align-items-stretch!"
+ :toggle-text="refShortName"
+ @search="setSearchTerm"
+ @select="setRefSelected"
+ @shown.once="loadRefs"
+ />
+</template>
diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/ci/pipeline_new/constants.js
index 43f7634083b..43f7634083b 100644
--- a/app/assets/javascripts/pipeline_new/constants.js
+++ b/app/assets/javascripts/ci/pipeline_new/constants.js
diff --git a/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql b/app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
index a76e8f6b95b..a76e8f6b95b 100644
--- a/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
index 648cd8b66b5..648cd8b66b5 100644
--- a/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/queries/ci_config_variables.graphql
diff --git a/app/assets/javascripts/pipeline_new/graphql/resolvers.js b/app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js
index 7b0f58e8cf9..7b0f58e8cf9 100644
--- a/app/assets/javascripts/pipeline_new/graphql/resolvers.js
+++ b/app/assets/javascripts/ci/pipeline_new/graphql/resolvers.js
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/ci/pipeline_new/index.js
index 71c76aeab36..71c76aeab36 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/ci/pipeline_new/index.js
diff --git a/app/assets/javascripts/pipeline_new/utils/filter_variables.js b/app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js
index 57ce3d13a9a..57ce3d13a9a 100644
--- a/app/assets/javascripts/pipeline_new/utils/filter_variables.js
+++ b/app/assets/javascripts/ci/pipeline_new/utils/filter_variables.js
diff --git a/app/assets/javascripts/ci/pipeline_new/utils/format_refs.js b/app/assets/javascripts/ci/pipeline_new/utils/format_refs.js
new file mode 100644
index 00000000000..e6d26b32d47
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_new/utils/format_refs.js
@@ -0,0 +1,55 @@
+import { __ } from '~/locale';
+import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '../constants';
+
+function convertToListBoxItems(items) {
+ return items.map(({ shortName, fullName }) => ({ text: shortName, value: fullName }));
+}
+
+export function formatRefs(refs, type) {
+ let fullName;
+
+ return refs.map((ref) => {
+ if (type === BRANCH_REF_TYPE) {
+ fullName = `refs/heads/${ref}`;
+ } else if (type === TAG_REF_TYPE) {
+ fullName = `refs/tags/${ref}`;
+ }
+
+ return {
+ shortName: ref,
+ fullName,
+ };
+ });
+}
+
+export const formatListBoxItems = (branches, tags) => {
+ const finalResults = [];
+
+ if (branches.length > 0) {
+ finalResults.push({
+ text: __('Branches'),
+ options: convertToListBoxItems(formatRefs(branches, BRANCH_REF_TYPE)),
+ });
+ }
+
+ if (tags.length > 0) {
+ finalResults.push({
+ text: __('Tags'),
+ options: convertToListBoxItems(formatRefs(tags, TAG_REF_TYPE)),
+ });
+ }
+
+ return finalResults;
+};
+
+export const searchByFullNameInListboxOptions = (fullName, listBox) => {
+ const optionsToSearch =
+ listBox.length > 1 ? listBox[0].options.concat(listBox[1].options) : listBox[0]?.options;
+
+ const foundOption = optionsToSearch.find(({ value }) => value === fullName);
+
+ return {
+ shortName: foundOption.text,
+ fullName: foundOption.value,
+ };
+};
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
index fe16cb7a92e..d03de91ea07 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules.vue
@@ -1,14 +1,25 @@
<script>
-import { GlAlert, GlBadge, GlButton, GlLoadingIcon, GlTabs, GlTab } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlBadge,
+ GlButton,
+ GlLoadingIcon,
+ GlTabs,
+ GlTab,
+ GlSprintf,
+ GlLink,
+} 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 playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
+import PipelineScheduleEmptyState from './pipeline_schedules_empty_state.vue';
export default {
i18n: {
@@ -16,11 +27,15 @@ export default {
scheduleDeleteError: s__(
'PipelineSchedules|There was a problem deleting the pipeline schedule.',
),
+ schedulePlayError: s__('PipelineSchedules|There was a problem playing 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.'),
+ playSuccess: s__(
+ 'PipelineSchedules|Successfully scheduled a pipeline to run. Go to the %{linkStart}Pipelines page%{linkEnd} for details. ',
+ ),
},
components: {
DeletePipelineScheduleModal,
@@ -30,13 +45,19 @@ export default {
GlLoadingIcon,
GlTabs,
GlTab,
+ GlSprintf,
+ GlLink,
PipelineSchedulesTable,
TakeOwnershipModal,
+ PipelineScheduleEmptyState,
},
inject: {
fullPath: {
default: '',
},
+ pipelinesPath: {
+ default: '',
+ },
},
apollo: {
schedules: {
@@ -68,6 +89,7 @@ export default {
},
scope,
hasError: false,
+ playSuccess: false,
errorMessage: '',
scheduleId: null,
showDeleteModal: false,
@@ -185,6 +207,27 @@ export default {
this.reportError(this.$options.i18n.takeOwnershipError);
}
},
+ async playPipelineSchedule(id) {
+ try {
+ const {
+ data: {
+ pipelineSchedulePlay: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: playPipelineScheduleMutation,
+ variables: { id },
+ });
+
+ if (errors.length > 0) {
+ throw new Error();
+ } else {
+ this.playSuccess = true;
+ }
+ } catch {
+ this.playSuccess = false;
+ this.reportError(this.$options.i18n.schedulePlayError);
+ }
+ },
fetchPipelineSchedulesByStatus(scope) {
this.scope = scope;
this.$apollo.queries.schedules.refetch();
@@ -195,62 +238,72 @@ export default {
<template>
<div>
- <gl-alert v-if="hasError" class="gl-mb-2" variant="danger" @dismiss="hasError = false">
+ <gl-alert v-if="hasError" class="gl-my-3" 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-alert v-if="playSuccess" class="gl-my-3" variant="info" @dismiss="playSuccess = false">
+ <gl-sprintf :message="$options.i18n.playSuccess">
+ <template #link="{ content }">
+ <gl-link :href="pipelinesPath" class="gl-text-decoration-none!">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+
+ <gl-tabs
+ v-if="isLoading || count > 0"
+ 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)"
>
- <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 #title>
+ <span>{{ tab.text }}</span>
- <template v-if="tab.showBadge">
- <gl-loading-icon v-if="tab.scope === scope && isLoading" class="gl-ml-2" />
+ <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>
+ <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>
+ <gl-loading-icon v-if="isLoading" size="lg" />
+ <pipeline-schedules-table
+ v-else
+ :schedules="schedules.list"
+ @showTakeOwnershipModal="setTakeOwnershipModal"
+ @showDeleteModal="setDeleteModal"
+ @playPipelineSchedule="playPipelineSchedule"
+ />
+ </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>
+ <template #tabs-end>
+ <gl-button variant="confirm" class="gl-ml-auto" data-testid="new-schedule-button">
+ {{ $options.i18n.newSchedule }}
+ </gl-button>
+ </template>
+ </gl-tabs>
+
+ <pipeline-schedule-empty-state v-else-if="!isLoading && count === 0" />
- <take-ownership-modal
- :visible="showTakeOwnershipModal"
- @takeOwnership="takeOwnership"
- @hideModal="hideModal"
- />
+ <take-ownership-modal
+ :visible="showTakeOwnershipModal"
+ @takeOwnership="takeOwnership"
+ @hideModal="hideModal"
+ />
- <delete-pipeline-schedule-modal
- :visible="showDeleteModal"
- @deleteSchedule="deleteSchedule"
- @hideModal="hideModal"
- />
- </template>
+ <delete-pipeline-schedule-modal
+ :visible="showDeleteModal"
+ @deleteSchedule="deleteSchedule"
+ @hideModal="hideModal"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
new file mode 100644
index 00000000000..f633ba053ee
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue
@@ -0,0 +1,63 @@
+<script>
+import scheduleSvg from '@gitlab/svgs/dist/illustrations/schedule-md.svg';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ pipelineSchedules: s__('PipelineSchedules|Pipeline schedules'),
+ description: s__(
+ 'PipelineSchedules|A scheduled pipeline starts automatically at regular intervals, like daily or weekly. The pipeline: ',
+ ),
+ learnMore: s__(
+ 'PipelineSchedules|Learn more in the %{linkStart}scheduled pipelines documentation.%{linkEnd}',
+ ),
+ listElements: [
+ s__('PipelineSchedules|Runs for a specific branch or tag.'),
+ s__('PipelineSchedules|Can have custom CI/CD variables.'),
+ s__('PipelineSchedules|Runs with the same project permissions as the schedule owner.'),
+ ],
+ createNew: s__('PipelineSchedules|Create a new pipeline schedule'),
+ },
+ components: {
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ },
+ computed: {
+ scheduleSvgPath() {
+ return `data:image/svg+xml;utf8,${encodeURIComponent(scheduleSvg)}`;
+ },
+ schedulesHelpPath() {
+ return helpPagePath('ci/pipelines/schedules');
+ },
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :svg-path="scheduleSvgPath"
+ :primary-button-text="$options.i18n.createNew"
+ primary-button-link="#"
+ >
+ <template #title>
+ <h3>
+ {{ $options.i18n.pipelineSchedules }}
+ </h3>
+ </template>
+ <template #description>
+ <p class="gl-mb-0">{{ $options.i18n.description }}</p>
+ <ul class="gl-list-style-position-inside" data-testid="pipeline-schedules-characteristics">
+ <li v-for="(el, index) in $options.i18n.listElements" :key="index">{{ el }}</li>
+ </ul>
+ <p>
+ <gl-sprintf :message="$options.i18n.learnMore">
+ <template #link="{ content }">
+ <gl-link :href="schedulesHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index a4ef7827f73..367b1812a27 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -71,7 +71,7 @@ export default {
timezone: this.cronTimezone,
formCiVariables: {},
// TODO: Add the GraphQL query to help populate the predefined variables
- // app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue#131
+ // app/assets/javascripts/ci/pipeline_new/components/pipeline_new_form.vue#131
predefinedValueOptions: {},
};
},
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
index 8656e5d3536..45b4f618e17 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -44,7 +44,14 @@ export default {
<template>
<div class="gl-display-flex gl-justify-content-end">
<gl-button-group>
- <gl-button v-if="canPlay" v-gl-tooltip :title="$options.i18n.playTooltip" icon="play" />
+ <gl-button
+ v-if="canPlay"
+ v-gl-tooltip
+ :title="$options.i18n.playTooltip"
+ icon="play"
+ data-testid="play-pipeline-schedule-btn"
+ @click="$emit('playPipelineSchedule', schedule.id)"
+ />
<gl-button
v-if="canTakeOwnership"
v-gl-tooltip
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index 216796b357c..56461165588 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,9 +1,9 @@
<script>
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
},
props: {
schedule: {
@@ -24,7 +24,11 @@ export default {
<template>
<div>
- <ci-badge v-if="hasPipeline" :status="lastPipelineStatus" class="gl-vertical-align-middle" />
+ <ci-badge-link
+ v-if="hasPipeline"
+ :status="lastPipelineStatus"
+ class="gl-vertical-align-middle"
+ />
<span v-else data-testid="pipeline-schedule-status-text">
{{ s__('PipelineSchedules|None') }}
</span>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
index 1b97a35a51e..e8cfc5b29f3 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue
@@ -96,6 +96,7 @@ export default {
:schedule="item"
@showTakeOwnershipModal="$emit('showTakeOwnershipModal', $event)"
@showDeleteModal="$emit('showDeleteModal', $event)"
+ @playPipelineSchedule="$emit('playPipelineSchedule', $event)"
/>
</template>
</gl-table-lite>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/play_pipeline_schedule.mutation.graphql b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/play_pipeline_schedule.mutation.graphql
new file mode 100644
index 00000000000..4892f41b93f
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/graphql/mutations/play_pipeline_schedule.mutation.graphql
@@ -0,0 +1,6 @@
+mutation playPipelineSchedule($id: CiPipelineScheduleID!) {
+ pipelineSchedulePlay(input: { id: $id }) {
+ clientMutationId
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
index 4c06fa321e5..8bca4f85e9f 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_app.js
@@ -18,7 +18,7 @@ export default () => {
return false;
}
- const { fullPath } = containerEl.dataset;
+ const { fullPath, pipelinesPath } = containerEl.dataset;
return new Vue({
el: containerEl,
@@ -26,6 +26,7 @@ export default () => {
apolloProvider,
provide: {
fullPath,
+ pipelinesPath,
},
render(createElement) {
return createElement(PipelineSchedules);
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index efa7909c913..e359344ab77 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { durationTimeFormatted } from '~/lib/utils/datetime_utility';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
@@ -11,7 +11,7 @@ import LinkCell from './cells/link_cell.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
GlTableLite,
LinkCell,
RunnerTags,
@@ -80,7 +80,7 @@ export default {
fixed
>
<template #cell(status)="{ item = {} }">
- <ci-badge v-if="item.detailedStatus" :status="item.detailedStatus" />
+ <ci-badge-link v-if="item.detailedStatus" :status="item.detailedStatus" />
</template>
<template #cell(job)="{ item = {} }">
diff --git a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
index 2e50dc13d2d..e0a6f4b1e67 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
@@ -23,6 +23,8 @@ export default {
RunnerSingleStat,
RunnerUpgradeStatusStats: () =>
import('ee_component/ci/runner/components/stat/runner_upgrade_status_stats.vue'),
+ RunnerPerformanceStat: () =>
+ import('ee_component/ci/runner/components/stat/runner_performance_stat.vue'),
},
props: {
scope: {
@@ -95,6 +97,8 @@ export default {
:scope="scope"
:variables="variables"
/>
+
+ <runner-performance-stat class="gl-px-5" />
</div>
</runner-count>
</template>
diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
index edfc22f644b..075dbb06190 100644
--- a/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/show/runner_jobs.query.graphql
@@ -8,7 +8,7 @@ query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String,
nodes {
id
detailedStatus {
- # fields for `<ci-badge>`
+ # fields for `<ci-badge-link>`
id
detailsPath
group
diff --git a/app/assets/javascripts/ci/runner/project_runners/index.js b/app/assets/javascripts/ci/runner/project_runners/index.js
new file mode 100644
index 00000000000..3be2b4a7422
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/project_runners/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import ProjectRunnersApp from './project_runners_app.vue';
+
+export const initProjectRunners = (selector = '#js-project-runners') => {
+ const el = document.querySelector(selector);
+
+ if (!el) {
+ return null;
+ }
+
+ const { projectFullPath } = el.dataset;
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(ProjectRunnersApp, {
+ props: {
+ projectFullPath,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue b/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue
new file mode 100644
index 00000000000..c7bf5e521a1
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/project_runners/project_runners_app.vue
@@ -0,0 +1,19 @@
+<script>
+export default {
+ props: {
+ projectFullPath: {
+ required: true,
+ type: String,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <!--
+ Under development
+ Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/33803
+ Feature rollout: https://gitlab.com/gitlab-org/gitlab/-/issues/386573
+ -->
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
index f2972133aad..3ea8e0df022 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/index.js
@@ -13,11 +13,6 @@ const parseJsonArray = (triggers) => {
export default (containerId = 'js-ci-pipeline-triggers-list') => {
const containerEl = document.getElementById(containerId);
- // Note: Remove this check when FF `ci_pipeline_triggers_settings_vue_ui` is removed.
- if (!containerEl) {
- return null;
- }
-
const triggers = parseJsonArray(containerEl.dataset.triggers);
return new Vue({
diff --git a/app/assets/javascripts/constants.js b/app/assets/javascripts/constants.js
index c56d45166a0..defc2cbe276 100644
--- a/app/assets/javascripts/constants.js
+++ b/app/assets/javascripts/constants.js
@@ -1,3 +1,6 @@
-import { s__ } from '~/locale';
+/* eslint-disable @gitlab/require-i18n-strings */
-export const MODIFIER_KEY = window.gl?.client?.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
+export const getModifierKey = (removeSuffix = false) => {
+ const winKey = `Ctrl${removeSuffix ? '' : '+'}`;
+ return window.gl?.client?.isMac ? '⌘' : winKey;
+};
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 53a37fc0c51..237808983ee 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -168,7 +168,12 @@ export default {
class="md-area"
:class="{ 'is-focused': focused }"
>
- <formatting-toolbar v-if="!useBottomToolbar" ref="toolbar" class="gl-border-b" />
+ <formatting-toolbar
+ v-if="!useBottomToolbar"
+ ref="toolbar"
+ class="gl-border-b"
+ @enableMarkdownEditor="$emit('enableMarkdownEditor')"
+ />
<div class="gl-relative gl-mt-4">
<formatting-bubble-menu />
<code-block-bubble-menu />
@@ -181,7 +186,12 @@ export default {
/>
<loading-indicator v-if="isLoading" />
</div>
- <formatting-toolbar v-if="useBottomToolbar" ref="toolbar" class="gl-border-t" />
+ <formatting-toolbar
+ v-if="useBottomToolbar"
+ ref="toolbar"
+ class="gl-border-t"
+ @enableMarkdownEditor="$emit('enableMarkdownEditor')"
+ />
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index 8a25ad3fd96..36ca3b8cfb6 100644
--- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -1,4 +1,5 @@
<script>
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
import trackUIControl from '../services/track_ui_control';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
@@ -9,6 +10,7 @@ import ToolbarMoreDropdown from './toolbar_more_dropdown.vue';
export default {
components: {
+ EditorModeDropdown,
ToolbarButton,
ToolbarTextStyleDropdown,
ToolbarLinkButton,
@@ -20,6 +22,11 @@ export default {
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ property: contentType, value });
},
+ handleEditorModeChanged(mode) {
+ if (mode === 'markdown') {
+ this.$emit('enableMarkdownEditor');
+ }
+ },
},
};
</script>
@@ -101,6 +108,8 @@ export default {
/>
<toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" />
<toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
+
+ <editor-mode-dropdown class="gl-ml-auto" value="richText" @input="handleEditorModeChanged" />
</div>
</template>
<style>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
index 2bf32a70cd1..9c1d1faca48 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
@@ -1,13 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { GlTooltipDirective as GlTooltip, GlCollapsibleListbox } from '@gitlab/ui';
import { __ } from '~/locale';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '../constants';
import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
EditorStateObserver,
},
directives: {
@@ -25,15 +24,26 @@ export default {
return activeItem ? activeItem.label : this.$options.i18n.placeholder;
},
+ listboxItems() {
+ return this.$options.items.map((item) => {
+ return {
+ value: item.label,
+ text: item.label,
+ };
+ });
+ },
},
methods: {
+ mapDropdownItemToCommand(dropdownItem) {
+ return this.$options.items.find((option) => option.label === dropdownItem);
+ },
updateActiveItem({ editor }) {
this.activeItem = TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
editor.isActive(item.contentType, item.commandParams),
);
},
execute(item) {
- const { editorCommand, contentType, commandParams } = item;
+ const { editorCommand, contentType, commandParams } = this.mapDropdownItemToCommand(item);
const value = commandParams?.level;
if (editorCommand) {
@@ -46,8 +56,8 @@ export default {
this.$emit('execute', { contentType, value });
},
- isActive(item) {
- return this.tiptapEditor.isActive(item.contentType, item.commandParams);
+ isActive(dropdownItem) {
+ return this.tiptapEditor.isActive(dropdownItem.contentType, dropdownItem.commandParams);
},
},
items: TEXT_STYLE_DROPDOWN_ITEMS,
@@ -58,25 +68,15 @@ export default {
</script>
<template>
<editor-state-observer @transaction="updateActiveItem">
- <gl-dropdown
- v-gl-tooltip="$options.i18n.placeholder"
- size="small"
- data-qa-selector="text_style_dropdown"
+ <gl-collapsible-listbox
+ v-gl-tooltip.hover="$options.i18n.placeholder"
+ :items="listboxItems"
+ :toggle-text="activeItemLabel"
+ :selected="activeItemLabel"
:disabled="!activeItem"
- :text="activeItemLabel"
- lazy
- >
- <gl-dropdown-item
- v-for="(item, index) in $options.items"
- :key="index"
- is-check-item
- :is-checked="isActive(item)"
- data-qa-selector="text_style_menu_item"
- :data-qa-text-style="item.label"
- @click="execute(item)"
- >
- {{ item.label }}
- </gl-dropdown-item>
- </gl-dropdown>
+ :data-qa-text-style="activeItemLabel"
+ data-qa-selector="text_style_dropdown"
+ @select="execute"
+ />
</editor-state-observer>
</template>
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
index c6aeb6c726d..9811a0774e1 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -11,7 +11,7 @@ import { debounce } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
-import statusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { __, s__ } from '~/locale';
import { queryTypes, formDataValidator } from '../constants';
@@ -23,7 +23,7 @@ function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
- if (resp.status === statusCodes.OK) {
+ if (resp.status === HTTP_STATUS_OK) {
stop(resp);
} else {
next();
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 48ab9ce0a3c..57fae608efa 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -288,7 +288,7 @@ export default {
</div>
</gl-form-group>
<div>
- <gl-button variant="success" @click="createDeployToken">
+ <gl-button variant="confirm" @click="createDeployToken">
{{ $options.translations.addTokenButton }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index c090a66a69d..8019a10a042 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -15,6 +15,7 @@ import Autosize from 'autosize';
import $ from 'jquery';
import { escape, uniqueId } from 'lodash';
import Vue from 'vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { createAlert, VARIANT_INFO } from '~/flash';
import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
@@ -40,7 +41,6 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash } from './lib/utils/url_utility';
import { sprintf, s__, __ } from './locale';
import TaskList from './task_list';
-import '~/behaviors/markdown/init_gfm';
window.autosize = Autosize;
@@ -516,7 +516,11 @@ export default class Notes {
}
if (discussionContainer.length === 0) {
if (noteEntity.diff_discussion_html) {
- const $discussion = $(noteEntity.diff_discussion_html).renderGFM();
+ const discussionElement = document.createElement('table');
+ // eslint-disable-next-line no-unsanitized/method
+ discussionElement.insertAdjacentHTML('afterbegin', noteEntity.diff_discussion_html);
+ renderGFM(discussionElement);
+ const $discussion = $(discussionElement).unwrap();
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder') || noteEntity.on_image) {
// insert the note and the reply button after the temp row
@@ -571,7 +575,9 @@ export default class Notes {
// reset text and preview
form.find('.js-md-write-button').click();
form.find('.js-note-text').val('').trigger('input');
- form.find('.js-note-text').data('autosave').reset();
+ form.find('.js-note-text').each(function reset() {
+ this.$autosave.reset();
+ });
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
@@ -638,7 +644,9 @@ export default class Notes {
// DiffNote
form.find('#note_position').val(),
];
- return new Autosave(textarea, key);
+ const textareaEl = textarea.get(0);
+ // eslint-disable-next-line no-new
+ if (textareaEl) new Autosave(textareaEl, key);
}
/**
@@ -708,7 +716,7 @@ export default class Notes {
$noteAvatar.append($targetNoteBadge);
this.revertNoteEditForm($targetNote);
- $noteEntityEl.renderGFM();
+ renderGFM($noteEntityEl.get(0));
// Find the note's `li` element by ID and replace it with the updated HTML
const $note_li = $(`.note-row-${noteEntity.id}`);
@@ -1082,7 +1090,9 @@ export default class Notes {
const row = form.closest('tr');
const glForm = form.data('glForm');
glForm.destroy();
- form.find('.js-note-text').data('autosave').reset();
+ form.find('.js-note-text').each(function reset() {
+ this.$autosave.reset();
+ });
// show the reply button (will only work for replies)
form.prev('.discussion-reply-holder').show();
if (row.is('.js-temp-notes-holder')) {
@@ -1382,7 +1392,8 @@ export default class Notes {
static animateAppendNote(noteHtml, $notesList) {
const $note = $(noteHtml);
- $note.addClass('fade-in-full').renderGFM();
+ $note.addClass('fade-in-full');
+ renderGFM($note.get(0));
$notesList.append($note);
return $note;
}
@@ -1390,7 +1401,8 @@ export default class Notes {
static animateUpdateNote(noteHtml, $note) {
const $updatedNote = $(noteHtml);
- $updatedNote.addClass('fade-in').renderGFM();
+ $updatedNote.addClass('fade-in');
+ renderGFM($updatedNote.get(0));
$note.replaceWith($updatedNote);
return $updatedNote;
}
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
index 5a6b220e532..830f16b50ee 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
-import $ from 'jquery';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import Autosave from '~/autosave';
@@ -118,7 +117,7 @@ export default {
},
initAutosaveComment() {
if (this.isLoggedIn) {
- this.autosaveDiscussion = new Autosave($(this.$refs.textarea), [
+ this.autosaveDiscussion = new Autosave(this.$refs.textarea, [
s__('DesignManagement|Discussion'),
getIdFromGraphQLId(this.noteableId),
this.shortDiscussionId,
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 23eb470503e..65816495432 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -12,9 +12,13 @@ const UNFOLD_COUNT = 20;
let isBound = false;
export default class Diff {
- constructor() {
+ constructor({ mergeRequestEventHub } = {}) {
const $diffFile = $('.files .diff-file');
+ if (mergeRequestEventHub) {
+ this.mrHub = mergeRequestEventHub;
+ }
+
$diffFile.each((index, file) => {
if (!$.data(file, 'singleFileDiff')) {
$.data(file, 'singleFileDiff', new SingleFileDiff(file));
@@ -34,7 +38,8 @@ export default class Diff {
$(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this))
.on('click', '.diff-line-num a', this.handleClickLineNum.bind(this))
- .on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this));
+ .on('mousedown', 'td.line_content.parallel', this.handleParallelLineDown.bind(this))
+ .on('click', '.inline-parallel-buttons a', ($e) => this.viewTypeSwitch($e));
isBound = true;
}
@@ -135,6 +140,20 @@ export default class Diff {
diffViewType() {
return $('.inline-parallel-buttons a.active').data('viewType');
}
+ viewTypeSwitch(event) {
+ const click = event.originalEvent;
+ const diffSource = new URL(click.target.getAttribute('href'), document.location.href);
+
+ if (this.mrHub) {
+ click.preventDefault();
+ click.stopPropagation();
+
+ diffSource.pathname = `${diffSource.pathname}.json`;
+
+ this.mrHub.$emit('diff:switch-view-type', { source: diffSource.toString() });
+ }
+ }
+
// eslint-disable-next-line class-methods-use-this
lineNumbers(line) {
const children = line.find('.diff-line-num').toArray();
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 7bc75127876..35d1a564178 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -389,10 +389,7 @@ export default {
() => {
this.setDiscussions();
- if (
- this.$store.state.notes.doneFetchingBatchDiscussions &&
- window.gon?.features?.paginatedMrDiscussions
- ) {
+ if (this.$store.state.notes.doneFetchingBatchDiscussions) {
this.unwatchDiscussions();
}
},
@@ -402,10 +399,6 @@ export default {
() => `${this.retrievingBatches}:${this.$store.state.notes.discussions.length}`,
() => {
if (!this.retrievingBatches && this.$store.state.notes.discussions.length) {
- if (!window.gon?.features?.paginatedMrDiscussions) {
- this.unwatchDiscussions();
- }
-
this.unwatchRetrievingBatches();
}
},
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index dff61acdfba..16f45c3ad6a 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -427,6 +427,7 @@ export default {
:href="diffFile.ide_edit_path"
class="js-ide-edit-blob"
data-qa-selector="edit_in_ide_button"
+ target="_blank"
>
{{ __('Open in Web IDE') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
index c37a1d75650..6cb1ed4cbcf 100644
--- a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
+++ b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
@@ -36,7 +36,7 @@ export default {
<p class="gl-mb-0">
{{
__(
- 'Resolve these conflicts or ask someone with write access to this repository to merge it locally.',
+ 'Resolve these conflicts, or ask someone with write access to this repository to resolve them locally.',
)
}}
</p>
@@ -54,7 +54,7 @@ export default {
v-gl-modal-directive="'modal-merge-info'"
class="gl-alert-action"
>
- {{ __('Merge locally') }}
+ {{ __('Resolve locally') }}
</gl-button>
</template>
</gl-alert>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index ffbea854001..abf77fa2ede 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -2,10 +2,13 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import micromatch from 'micromatch';
+import { getModifierKey } from '~/constants';
import { s__, sprintf } from '~/locale';
import FileTree from '~/vue_shared/components/file_tree.vue';
import DiffFileRow from './diff_file_row.vue';
+const MODIFIER_KEY = getModifierKey();
+
export default {
directives: {
GlTooltip: GlTooltipDirective,
@@ -65,8 +68,8 @@ export default {
this.search = '';
},
},
- searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{modifier_key}P)'), {
- modifier_key: /Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl+',
+ searchPlaceholder: sprintf(s__('MergeRequest|Search (e.g. *.vue) (%{MODIFIER_KEY}P)'), {
+ MODIFIER_KEY,
}),
DiffFileRow,
};
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 96a73917820..9f90de9abde 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -9,7 +9,7 @@ import { createAlert, VARIANT_WARNING } from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -232,7 +232,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => {
.catch((error) => {
worker.terminate();
- if (error.response.status === httpStatusCodes.NOT_FOUND) {
+ if (error.response.status === HTTP_STATUS_NOT_FOUND) {
createAlert({
message: __('Building your merge request. Wait a few moments, then refresh this page.'),
variant: VARIANT_WARNING,
@@ -248,7 +248,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
data: state.endpointCoverage,
method: 'getCoverageReports',
successCallback: ({ status, data }) => {
- if (status === httpStatusCodes.OK) {
+ if (status === HTTP_STATUS_OK) {
commit(types.SET_COVERAGE_DATA, data);
coveragePoll.stop();
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index d0649ecccba..d235319dfd7 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,7 +1,9 @@
-import { MODIFIER_KEY } from '~/constants';
+import { getModifierKey } from '~/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
+const modifierKey = getModifierKey();
+
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
@@ -67,7 +69,7 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
{
id: 'bold',
label: sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
- modifierKey: MODIFIER_KEY,
+ modifierKey,
}),
data: {
mdTag: '**',
@@ -77,7 +79,7 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
{
id: 'italic',
label: sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
- modifierKey: MODIFIER_KEY,
+ modifierKey,
}),
data: {
mdTag: '_',
@@ -87,7 +89,7 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
{
id: 'strikethrough',
label: sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
- modifierKey: MODIFIER_KEY,
+ modifierKey,
}),
data: {
mdTag: '~~',
@@ -113,7 +115,7 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
{
id: 'link',
label: sprintf(s__('MarkdownEditor|Add a link (%{modifier_key}K)'), {
- modifierKey: MODIFIER_KEY,
+ modifierKey,
}),
data: {
mdTag: '[{text}](url)',
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index d94aa73e43a..87d869cc996 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -239,8 +239,15 @@
}
]
},
+ "browser_performance": {
+ "type": "string",
+ "description": "Path to a single file with browser performance metric report(s)."
+ },
"coverage_report": {
- "type": "object",
+ "type": [
+ "object",
+ "null"
+ ],
"description": "Used to collect coverage reports from the job.",
"properties": {
"coverage_format": {
@@ -292,10 +299,6 @@
"$ref": "#/definitions/string_file_list",
"description": "Path to file or list of files with license report(s)."
},
- "performance": {
- "$ref": "#/definitions/string_file_list",
- "description": "Path to file or list of files with performance metrics report(s)."
- },
"requirements": {
"$ref": "#/definitions/string_file_list",
"description": "Path to file or list of files with requirements report(s)."
@@ -703,7 +706,10 @@
}
},
"rules": {
- "type": "array",
+ "type": [
+ "array",
+ "null"
+ ],
"markdownDescription": "Rules allows for an array of individual rule objects to be evaluated in order, until one matches and dynamically provides attributes to the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rules).",
"items": {
"anyOf": [
@@ -994,6 +1000,11 @@
"pull-push"
]
},
+ "unprotect": {
+ "type": "boolean",
+ "markdownDescription": "Use `unprotect: true` to set a cache to be shared between protected and unprotected branches.",
+ "default": false
+ },
"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)",
@@ -1601,7 +1612,7 @@
"description": "Creates N instances of the same job that run in parallel.",
"default": 0,
"minimum": 2,
- "maximum": 50
+ "maximum": 200
},
{
"type": "object",
@@ -1620,7 +1631,7 @@
]
}
},
- "maxItems": 50
+ "maxItems": 200
}
},
"additionalProperties": false,
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_job.vue b/app/assets/javascripts/environments/environment_details/components/deployment_job.vue
new file mode 100644
index 00000000000..dbe25a81550
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_job.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlTruncate, GlLink, GlBadge } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlBadge,
+ GlTruncate,
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <gl-link v-if="job" :href="job.webPath">
+ <gl-truncate :text="job.label" />
+ </gl-link>
+ <gl-badge v-else variant="info">{{ __('API') }}</gl-badge>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue b/app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue
new file mode 100644
index 00000000000..82926e2e596
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue
@@ -0,0 +1,26 @@
+<script>
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+
+export default {
+ components: {
+ DeploymentStatusBadge,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ deploymentJob: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <a v-if="deploymentJob" data-testid="deployment-status-job-link" :href="deploymentJob.webPath">
+ <deployment-status-badge :status="status" />
+ </a>
+ <deployment-status-badge v-else :status="status" />
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue b/app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue
new file mode 100644
index 00000000000..18ff31f9b0f
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue
@@ -0,0 +1,25 @@
+<script>
+import { GlAvatar, GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAvatar,
+ GlAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ triggerer: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <gl-avatar-link v-if="triggerer" :href="triggerer.webUrl">
+ <gl-avatar v-gl-tooltip :title="triggerer.name" :src="triggerer.avatarUrl" :size="24" />
+ </gl-avatar-link>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/constants.js b/app/assets/javascripts/environments/environment_details/constants.js
index 56c70c354b7..bf690ffedeb 100644
--- a/app/assets/javascripts/environments/environment_details/constants.js
+++ b/app/assets/javascripts/environments/environment_details/constants.js
@@ -1,4 +1,5 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const ENVIRONMENT_DETAILS_PAGE_SIZE = 20;
export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
@@ -45,3 +46,17 @@ export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
},
];
+
+export const translations = {
+ emptyStateTitle: s__("Deployments|You don't have any deployments right now."),
+ emptyStatePrimaryButton: __('Read more'),
+ emptyStateDescription: s__(
+ 'Deployments|Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here.',
+ ),
+ nextPageButtonLabel: __('Next'),
+ previousPageButtonLabel: __('Prev'),
+};
+
+export const codeBlockPlaceholders = { code: ['code_open', 'code_close'] };
+
+export const environmentsHelpPagePath = helpPagePath('ci/environments/index.md');
diff --git a/app/assets/javascripts/environments/environment_details/deployments_table.vue b/app/assets/javascripts/environments/environment_details/deployments_table.vue
new file mode 100644
index 00000000000..41570ee44c0
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/deployments_table.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlTableLite } from '@gitlab/ui';
+import Commit from '~/vue_shared/components/commit.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import DeploymentStatusLink from './components/deployment_status_link.vue';
+import DeploymentJob from './components/deployment_job.vue';
+import DeploymentTriggerer from './components/deployment_triggerer.vue';
+import { ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
+
+export default {
+ components: {
+ DeploymentTriggerer,
+ DeploymentJob,
+ Commit,
+ TimeAgoTooltip,
+ DeploymentStatusLink,
+ GlTableLite,
+ },
+ props: {
+ deployments: {
+ type: Array,
+ required: true,
+ },
+ },
+ tableFields: ENVIRONMENT_DETAILS_TABLE_FIELDS,
+};
+</script>
+<template>
+ <gl-table-lite :items="deployments" :fields="$options.tableFields" fixed stacked="lg">
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+ <template #cell(status)="{ item }">
+ <deployment-status-link :deployment-job="item.job" :status="item.status" />
+ </template>
+ <template #cell(id)="{ item }">
+ <strong>{{ item.id }}</strong>
+ </template>
+ <template #cell(triggerer)="{ item }">
+ <deployment-triggerer :triggerer="item.triggerer" />
+ </template>
+ <template #cell(commit)="{ item }">
+ <commit v-bind="item.commit" />
+ </template>
+ <template #cell(job)="{ item }">
+ <deployment-job :job="item.job" />
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago-tooltip :time="item.created" />
+ </template>
+ <template #cell(deployed)="{ item }">
+ <time-ago-tooltip :time="item.deployed" />
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/empty_state.vue b/app/assets/javascripts/environments/environment_details/empty_state.vue
new file mode 100644
index 00000000000..6f08b319408
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/empty_state.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlEmptyState, GlSprintf } from '@gitlab/ui';
+import { translations, codeBlockPlaceholders, environmentsHelpPagePath } from './constants';
+
+export default {
+ components: {
+ GlSprintf,
+ GlEmptyState,
+ },
+ translations,
+ actionButtonUrl: environmentsHelpPagePath,
+ placeholders: {
+ code: codeBlockPlaceholders,
+ },
+};
+</script>
+<template>
+ <gl-empty-state
+ :title="$options.translations.emptyStateTitle"
+ :primary-button-text="$options.translations.emptyStatePrimaryButton"
+ :primary-button-link="$options.actionButtonUrl"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="$options.translations.emptyStateDescription"
+ :placeholders="$options.placeholders.code"
+ >
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
index 435d3fd820e..b43f4233b9c 100644
--- a/app/assets/javascripts/environments/environment_details/index.vue
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -1,36 +1,19 @@
<script>
-import {
- GlTableLite,
- GlAvatarLink,
- GlAvatar,
- GlLink,
- GlTooltipDirective,
- GlTruncate,
- GlBadge,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import Commit from '~/vue_shared/components/commit.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { logError } from '~/lib/logger';
import environmentDetailsQuery from '../graphql/queries/environment_details.query.graphql';
import { convertToDeploymentTableRow } from '../helpers/deployment_data_transformation_helper';
-import DeploymentStatusBadge from '../components/deployment_status_badge.vue';
-import { ENVIRONMENT_DETAILS_PAGE_SIZE, ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
+import EmptyState from './empty_state.vue';
+import DeploymentsTable from './deployments_table.vue';
+import Pagination from './pagination.vue';
+import { ENVIRONMENT_DETAILS_PAGE_SIZE } from './constants';
export default {
components: {
+ Pagination,
+ DeploymentsTable,
+ EmptyState,
GlLoadingIcon,
- GlBadge,
- DeploymentStatusBadge,
- TimeAgoTooltip,
- GlTableLite,
- GlAvatarLink,
- GlAvatar,
- GlLink,
- GlTruncate,
- Commit,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
},
props: {
projectFullPath: {
@@ -41,6 +24,16 @@ export default {
type: String,
required: true,
},
+ after: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ before: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
apollo: {
project: {
@@ -49,18 +42,19 @@ export default {
return {
projectFullPath: this.projectFullPath,
environmentName: this.environmentName,
- pageSize: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ first: this.before ? null : ENVIRONMENT_DETAILS_PAGE_SIZE,
+ last: this.before ? ENVIRONMENT_DETAILS_PAGE_SIZE : null,
+ after: this.after,
+ before: this.before,
};
},
},
},
data() {
return {
- project: {
- loading: true,
- },
- loading: 0,
- tableFields: ENVIRONMENT_DETAILS_TABLE_FIELDS,
+ project: {},
+ isInitialPageDataReceived: false,
+ isPrefetchingPages: false,
};
},
computed: {
@@ -70,49 +64,80 @@ export default {
isLoading() {
return this.$apollo.queries.project.loading;
},
+ isDeploymentTableShown() {
+ return this.isInitialPageDataReceived === true && this.deployments.length > 0;
+ },
+ pageInfo() {
+ return this.project.environment?.deployments.pageInfo || {};
+ },
+ isPaginationDisabled() {
+ return this.isLoading || this.isPrefetchingPages;
+ },
+ },
+ watch: {
+ async project(newProject) {
+ this.isInitialPageDataReceived = true;
+ this.isPrefetchingPages = true;
+
+ try {
+ // TLDR: when we load a page, if there's next and/or previous pages existing, we'll load their data as well to improve percepted performance.
+ const {
+ endCursor,
+ hasPreviousPage,
+ hasNextPage,
+ startCursor,
+ } = newProject.environment.deployments.pageInfo;
+
+ // At the moment we have a limit of deployments being requested only from a signle environment entity per query,
+ // and apparently two batched queries count as one on server-side
+ // to load both next and previous page data, we have to query them sequentially
+ if (hasNextPage) {
+ await this.$apollo.query({
+ query: environmentDetailsQuery,
+ variables: {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.environmentName,
+ first: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ after: endCursor,
+ before: null,
+ last: null,
+ },
+ });
+ }
+
+ if (hasPreviousPage) {
+ await this.$apollo.query({
+ query: environmentDetailsQuery,
+ variables: {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.environmentName,
+ first: null,
+ after: null,
+ before: startCursor,
+ last: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ },
+ });
+ }
+ } catch (error) {
+ logError(error);
+ }
+
+ this.isPrefetchingPages = false;
+ },
},
};
</script>
<template>
- <div>
- <gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
- <gl-table-lite v-else :items="deployments" :fields="tableFields" fixed stacked="lg">
- <template #table-colgroup="{ fields }">
- <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
- </template>
- <template #cell(status)="{ item }">
- <div>
- <deployment-status-badge :status="item.status" />
- </div>
- </template>
- <template #cell(id)="{ item }">
- <strong>{{ item.id }}</strong>
- </template>
- <template #cell(triggerer)="{ item }">
- <gl-avatar-link :href="item.triggerer.webUrl">
- <gl-avatar
- v-gl-tooltip
- :title="item.triggerer.name"
- :src="item.triggerer.avatarUrl"
- :size="24"
- />
- </gl-avatar-link>
- </template>
- <template #cell(commit)="{ item }">
- <commit v-bind="item.commit" />
- </template>
- <template #cell(job)="{ item }">
- <gl-link v-if="item.job" :href="item.job.webPath">
- <gl-truncate :text="item.job.label" />
- </gl-link>
- <gl-badge v-else variant="info">{{ __('API') }}</gl-badge>
- </template>
- <template #cell(created)="{ item }">
- <time-ago-tooltip :time="item.created" />
- </template>
- <template #cell(deployed)="{ item }">
- <time-ago-tooltip :time="item.deployed" />
- </template>
- </gl-table-lite>
+ <div class="gl-relative gl-min-h-6">
+ <div
+ v-if="isLoading"
+ class="gl-absolute gl-top-0 gl-left-0 gl-w-full gl-h-full gl-z-index-200 gl-bg-gray-10 gl-opacity-3"
+ ></div>
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-absolute gl-top-half gl-left-50p" />
+ <div v-if="isDeploymentTableShown">
+ <deployments-table :deployments="deployments" />
+ <pagination :page-info="pageInfo" :disabled="isPaginationDisabled" />
+ </div>
+ <empty-state v-if="!isDeploymentTableShown && !isLoading" />
</div>
</template>
diff --git a/app/assets/javascripts/environments/environment_details/pagination.vue b/app/assets/javascripts/environments/environment_details/pagination.vue
new file mode 100644
index 00000000000..414610b306a
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/pagination.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { translations } from './constants';
+
+export default {
+ components: {
+ GlKeysetPagination,
+ },
+ props: {
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ translations,
+ computed: {
+ previousLink() {
+ if (!this.pageInfo || !this.pageInfo.hasPreviousPage) {
+ return '';
+ }
+ return setUrlParams({ before: this.pageInfo.startCursor }, window.location.href, true);
+ },
+ nextLink() {
+ if (!this.pageInfo || !this.pageInfo.hasNextPage) {
+ return '';
+ }
+ return setUrlParams({ after: this.pageInfo.endCursor }, window.location.href, true);
+ },
+ isPaginationVisible() {
+ if (!this.pageInfo) {
+ return false;
+ }
+
+ return this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage;
+ },
+ },
+ methods: {
+ onPrev(previousCursor) {
+ this.$router.push({ query: { before: previousCursor } });
+ },
+ onNext(nextCursor) {
+ this.$router.push({ query: { after: nextCursor } });
+ },
+ onPaginationClick(event) {
+ // this check here is to ensure the proper default behvaior when a user ctrl/cmd + clicks the link
+ if (event.shiftKey || event.ctrlKey || event.altKey || event.metaKey) {
+ return;
+ }
+ event.preventDefault();
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="isPaginationVisible" class="gl--flex-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="$options.translations.previousPageButtonLabel"
+ :next-text="$options.translations.nextPageButtonLabel"
+ :prev-button-link="previousLink"
+ :next-button-link="nextLink"
+ :disabled="disabled"
+ @prev="onPrev"
+ @next="onNext"
+ @click="onPaginationClick"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
index e8f2a2cdf7f..c6c2024c840 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
@@ -1,4 +1,11 @@
-query getEnvironmentDetails($projectFullPath: ID!, $environmentName: String, $pageSize: Int) {
+query getEnvironmentDetails(
+ $projectFullPath: ID!
+ $environmentName: String
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
project(fullPath: $projectFullPath) {
id
name
@@ -6,7 +13,19 @@ query getEnvironmentDetails($projectFullPath: ID!, $environmentName: String, $pa
environment(name: $environmentName) {
id
name
- deployments(orderBy: { createdAt: DESC }, first: $pageSize) {
+ deployments(
+ orderBy: { createdAt: DESC }
+ first: $first
+ last: $last
+ after: $after
+ before: $before
+ ) {
+ pageInfo {
+ startCursor
+ endCursor
+ hasNextPage
+ hasPreviousPage
+ }
nodes {
id
iid
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index ba816599ac2..afce2b7f237 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
import { apolloProvider } from './graphql/client';
@@ -43,7 +44,7 @@ export const initHeader = () => {
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
terminalPath: dataset.environmentTerminalPath,
metricsPath: dataset.environmentMetricsPath,
- updatePath: dataset.tnvironmentEditPath,
+ updatePath: dataset.environmentEditPath,
},
});
},
@@ -60,18 +61,40 @@ export const initPage = async () => {
const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
Vue.use(VueApollo);
+ Vue.use(VueRouter);
const el = document.getElementById('environment_details_page');
+
+ const router = new VueRouter({
+ mode: 'history',
+ base: window.location.pathname,
+ routes: [
+ {
+ path: '/',
+ name: 'environment_details',
+ component: EnvironmentsDetailPage,
+ props: (route) => ({
+ after: route.query.after,
+ before: route.query.before,
+ projectFullPath: dataSet.projectFullPath,
+ environmentName: dataSet.name,
+ }),
+ },
+ ],
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return { top: 0 };
+ },
+ });
+
return new Vue({
el,
apolloProvider: apolloProvider(),
+ router,
provide: {},
render(createElement) {
- return createElement(EnvironmentsDetailPage, {
- props: {
- projectFullPath: dataSet.projectFullPath,
- environmentName: dataSet.name,
- },
- });
+ return createElement('router-view');
},
});
};
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 122c7c005e9..2a4bb88b6c2 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -161,7 +161,7 @@ export default {
return this.pagination.next ? this.$options.NEXT_PAGE : null;
},
errorTrackingHelpUrl() {
- return helpPagePath('operations/error_tracking');
+ return helpPagePath('operations/error_tracking.html#integrated-error-tracking');
},
showIntegratedDisabledAlert() {
return !this.isAlertDismissed && this.showIntegratedTrackingDisabledAlert;
@@ -175,6 +175,7 @@ export default {
},
},
epicLink: 'https://gitlab.com/gitlab-org/gitlab/-/issues/353639',
+ openBetaLink: 'https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta',
featureFlagLink: helpPagePath('operations/error_tracking'),
created() {
if (this.errorTrackingEnabled) {
@@ -454,24 +455,19 @@ export default {
/>
</template>
</div>
- <div v-else-if="userCanEnableErrorTracking">
- <gl-empty-state
- :title="__('Get started with error tracking')"
- :description="__('Monitor your errors by integrating with Sentry.')"
- :primary-button-text="__('Enable error tracking')"
- :primary-button-link="enableErrorTrackingLink"
- :svg-path="illustrationPath"
- />
- </div>
<div v-else>
<gl-empty-state :title="__('Get started with error tracking')" :svg-path="illustrationPath">
<template #description>
<div>
- <span>{{ __('Monitor your errors by integrating with Sentry.') }}</span>
+ <span>{{ __('Monitor your errors directly in GitLab.') }}</span>
<gl-link target="_blank" :href="errorTrackingHelpUrl">{{
- __('More information')
+ __('How do I get started?')
}}</gl-link>
</div>
+ <div class="gl-mt-3">
+ <span>{{ __('Error tracking is currently in') }}</span>
+ <gl-link target="_blank" :href="$options.openBetaLink">{{ __('Open Beta.') }}</gl-link>
+ </div>
</template>
</gl-empty-state>
</div>
diff --git a/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
index 2323370a3aa..cd101f57d4f 100644
--- a/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/project_dropdown.vue
@@ -1,11 +1,10 @@
<script>
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { getDisplayName } from '../utils';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
},
props: {
dropdownLabel: {
@@ -42,8 +41,21 @@ export default {
required: true,
},
},
+ computed: {
+ listboxItems() {
+ return this.projects.map((project) => {
+ return {
+ text: getDisplayName(project),
+ value: project.id,
+ };
+ });
+ },
+ },
methods: {
- getDisplayName,
+ selectProject(id) {
+ const project = this.projects.find((p) => p.id === id);
+ this.$emit('select-project', project);
+ },
},
};
</script>
@@ -52,22 +64,15 @@ export default {
<div :class="{ 'gl-show-field-errors': isProjectInvalid }">
<label class="label-bold" for="project-dropdown">{{ __('Project') }}</label>
<div class="row">
- <gl-dropdown
+ <gl-collapsible-listbox
id="project-dropdown"
- class="col-8 col-md-9 gl-pr-0"
+ class="gl-pl-5"
:disabled="!hasProjects"
- menu-class="w-100 mw-100"
- toggle-class="dropdown-menu-toggle gl-field-error-outline"
- :text="dropdownLabel"
- >
- <gl-dropdown-item
- v-for="project in projects"
- :key="`${project.organizationSlug}.${project.slug}`"
- class="w-100"
- @click="$emit('select-project', project)"
- >{{ getDisplayName(project) }}</gl-dropdown-item
- >
- </gl-dropdown>
+ :items="listboxItems"
+ :selected="selectedProject && selectedProject.id"
+ :toggle-text="dropdownLabel"
+ @select="selectProject"
+ />
</div>
<p v-if="isProjectInvalid" class="js-project-dropdown-error gl-field-error">
{{ invalidProjectLabel }}
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 9e804b60d59..cebf73ef8e5 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,9 +1,7 @@
import * as Sentry from '@sentry/browser';
-import { escape } from 'lodash';
import Vue from 'vue';
import { GlAlert } from '@gitlab/ui';
import { __ } from '~/locale';
-import { spriteIcon } from './lib/utils/common_utils';
const FLASH_TYPES = {
ALERT: 'alert',
@@ -18,13 +16,6 @@ const VARIANT_DANGER = 'danger';
const VARIANT_INFO = 'info';
const VARIANT_TIP = 'tip';
-const TYPE_TO_VARIANT = {
- [FLASH_TYPES.ALERT]: VARIANT_DANGER,
- [FLASH_TYPES.NOTICE]: VARIANT_INFO,
- [FLASH_TYPES.SUCCESS]: VARIANT_SUCCESS,
- [FLASH_TYPES.WARNING]: VARIANT_WARNING,
-};
-
const FLASH_CLOSED_EVENT = 'flashClosed';
const getCloseEl = (flashEl) => {
@@ -57,27 +48,6 @@ const hideFlash = (flashEl, fadeTransition = true) => {
if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
};
-const createAction = (config) => `
- <a
- href="${config.href || '#'}"
- class="flash-action"
- ${config.href ? '' : 'role="button"'}
- >
- ${escape(config.title)}
- </a>
-`;
-
-const createFlashEl = (message, type) => `
- <div class="flash-${type}" data-testid="alert-${TYPE_TO_VARIANT[type]}">
- <div class="flash-text">
- ${escape(message)}
- <div class="close-icon-wrapper js-close-icon">
- ${spriteIcon('close', 'close-icon')}
- </div>
- </div>
- </div>
-`;
-
const addDismissFlashClickListener = (flashEl, fadeTransition) => {
// There are some flash elements which do not have a closeEl.
// https://gitlab.com/gitlab-org/gitlab/blob/763426ef344488972eb63ea5be8744e0f8459e6b/ee/app/views/layouts/header/_read_only_banner.html.haml
@@ -211,73 +181,7 @@ const createAlert = function createAlert({
});
};
-/**
- * @deprecated use `createAlert` instead
- *
- * Flash banner supports different types of Flash configurations
- * along with ability to provide actionConfig which can be used to show
- * additional action or link on banner next to message
- *
- * @param {object} options - Options to control the flash message
- * @param {string} options.message - Flash message text
- * @param {'alert'|'notice'|'success'|'warning'} [options.type] - Type of Flash; it defaults to 'alert'
- * @param {Element|Document} [options.parent] - Reference to parent element under which Flash needs to appear
- * @param {object} [options.actionConfig] - Map of config to show action on banner
- * @param {string} [options.actionConfig.href] - URL to which action config should point to (default: '#')
- * @param {string} [options.actionConfig.title] - Title of action
- * @param {Function} [options.actionConfig.clickHandler] - Method to call when action is clicked on
- * @param {boolean} [options.fadeTransition] - Boolean to determine whether to fade the alert out
- * @param {boolean} [options.addBodyClass] - Adds `flash-shown` class to the `body` element
- * @param {boolean} [options.captureError] - Boolean to determine whether to send error to Sentry
- * @param {object} [options.error] - Error to be captured in Sentry
- */
-const createFlash = function createFlash({
- message,
- type = FLASH_TYPES.ALERT,
- parent = document,
- actionConfig = null,
- fadeTransition = true,
- addBodyClass = false,
- captureError = false,
- error = null,
-}) {
- const flashContainer = parent.querySelector('.flash-container');
-
- if (!flashContainer) return null;
-
- // eslint-disable-next-line no-unsanitized/property
- flashContainer.innerHTML = createFlashEl(message, type);
-
- const flashEl = flashContainer.querySelector(`.flash-${type}`);
-
- if (actionConfig) {
- // eslint-disable-next-line no-unsanitized/method
- flashEl.insertAdjacentHTML('beforeend', createAction(actionConfig));
-
- if (actionConfig.clickHandler) {
- flashEl
- .querySelector('.flash-action')
- .addEventListener('click', (e) => actionConfig.clickHandler(e));
- }
- }
-
- addDismissFlashClickListener(flashEl, fadeTransition);
-
- flashContainer.classList.add('gl-display-block');
-
- if (addBodyClass) document.body.classList.add('flash-shown');
-
- if (captureError && error) Sentry.captureException(error);
-
- flashContainer.close = () => {
- getCloseEl(flashEl).click();
- };
-
- return flashContainer;
-};
-
export {
- createFlash as default,
hideFlash,
addDismissFlashClickListener,
FLASH_TYPES,
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 8ad9eeaa266..a4e883c96b5 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import AccessorUtilities from '~/lib/utils/accessor';
import {
mapVuexModuleState,
@@ -18,6 +18,11 @@ export default {
FrequentItemsSearchInput,
FrequentItemsList,
GlLoadingIcon,
+ GlButton,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
mixins: [frequentItemsMixin],
inject: ['vuexModule'],
@@ -40,12 +45,14 @@ export default {
...mapVuexModuleState((vm) => vm.vuexModule, [
'searchQuery',
'isLoadingItems',
+ 'isItemsListEditable',
'isFetchFailed',
+ 'isItemRemovalFailed',
'items',
]),
...mapVuexModuleGetters((vm) => vm.vuexModule, ['hasSearchQuery']),
translations() {
- return this.getTranslations(['loadingMessage', 'header']);
+ return this.getTranslations(['loadingMessage', 'header', 'headerEditToggle']);
},
},
created() {
@@ -74,6 +81,7 @@ export default {
...mapVuexModuleActions((vm) => vm.vuexModule, [
'setNamespace',
'setStorageKey',
+ 'toggleItemsListEditablity',
'fetchFrequentItems',
]),
dropdownOpenHandler() {
@@ -132,8 +140,25 @@ export default {
class="loading-animation prepend-top-20"
data-testid="loading"
/>
- <div v-if="!isLoadingItems && !hasSearchQuery" class="section-header" data-testid="header">
- {{ translations.header }}
+ <div
+ v-if="!isLoadingItems && !hasSearchQuery"
+ class="section-header gl-display-flex"
+ data-testid="header"
+ >
+ <span class="gl-flex-grow-1">{{ translations.header }}</span>
+ <gl-button
+ v-if="items.length"
+ v-gl-tooltip.left
+ size="small"
+ category="tertiary"
+ :aria-label="translations.headerEditToggle"
+ :title="translations.headerEditToggle"
+ :class="{ 'gl-bg-gray-100!': isItemsListEditable }"
+ class="gl-p-2!"
+ @click="toggleItemsListEditablity"
+ >
+ <gl-icon name="pencil" :class="{ 'gl-text-gray-900!': isItemsListEditable }" />
+ </gl-button>
</div>
<frequent-items-list
v-if="!isLoadingItems"
@@ -141,6 +166,7 @@ export default {
:namespace="namespace"
:has-search-query="hasSearchQuery"
:is-fetch-failed="isFetchFailed"
+ :is-item-removal-failed="isItemRemovalFailed"
:matcher="searchQuery"
/>
</div>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
index c0bfcf9c4a9..da1d3bedaf4 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -21,6 +21,10 @@ export default {
type: Boolean,
required: true,
},
+ isItemRemovalFailed: {
+ type: Boolean,
+ required: true,
+ },
matcher: {
type: String,
required: true,
@@ -38,6 +42,9 @@ export default {
isListEmpty() {
return this.items.length === 0;
},
+ showListEmptyMessage() {
+ return this.isListEmpty || this.isItemRemovalFailed;
+ },
listEmptyMessage() {
if (this.hasSearchQuery) {
return this.isFetchFailed
@@ -45,7 +52,7 @@ export default {
: this.translations.searchListEmptyMessage;
}
- return this.isFetchFailed
+ return this.isFetchFailed || this.isItemRemovalFailed
? this.translations.itemListErrorMessage
: this.translations.itemListEmptyMessage;
},
@@ -60,9 +67,10 @@ export default {
<div class="frequent-items-list-container">
<ul data-testid="frequent-items-list" class="list-unstyled">
<li
- v-if="isListEmpty"
+ v-if="showListEmptyMessage"
:class="{ 'section-failure': isFetchFailed }"
class="section-empty gl-mb-3"
+ data-testid="frequent-items-list-empty"
>
{{ listEmptyMessage }}
</li>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 89b6885091c..75ea9beb5cf 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,10 +1,10 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { snakeCase } from 'lodash';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
-import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
+import { mapVuexModuleState, mapVuexModuleActions } from '~/lib/utils/vuex_module_mappers';
import Tracking from '~/tracking';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
@@ -12,11 +12,13 @@ const trackingMixin = Tracking.mixin();
export default {
components: {
+ GlIcon,
GlButton,
ProjectAvatar,
},
directives: {
SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
mixins: [trackingMixin],
inject: ['vuexModule'],
@@ -51,7 +53,7 @@ export default {
},
},
computed: {
- ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType']),
+ ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType', 'isItemsListEditable']),
truncatedNamespace() {
return truncateNamespace(this.namespace);
},
@@ -62,43 +64,63 @@ export default {
return `${this.dropdownType}_dropdown_frequent_items_list_item_${snakeCase(this.itemName)}`;
},
},
+ methods: {
+ ...mapVuexModuleActions((vm) => vm.vuexModule, ['removeFrequentItem']),
+ },
};
</script>
<template>
- <li class="frequent-items-list-item-container">
+ <li class="frequent-items-list-item-container gl-relative">
<gl-button
category="tertiary"
:href="webUrl"
- class="gl-text-left gl-justify-content-start!"
+ class="gl-text-left gl-w-full"
+ button-text-classes="gl-display-flex gl-w-full"
+ data-testid="frequent-item-link"
@click="track('click_link', { label: itemTrackingLabel })"
>
- <project-avatar
- class="gl-float-left gl-mr-3"
- :project-avatar-url="avatarUrl"
- :project-id="itemId"
- :project-name="itemName"
- aria-hidden="true"
- />
- <div
- data-testid="frequent-items-item-metadata-container"
- class="frequent-items-item-metadata-container"
- >
- <div
- v-safe-html="highlightedItemName"
- data-testid="frequent-items-item-title"
- :title="itemName"
- class="frequent-items-item-title"
- ></div>
+ <div class="gl-flex-grow-1">
+ <project-avatar
+ class="gl-float-left gl-mr-3"
+ :project-avatar-url="avatarUrl"
+ :project-id="itemId"
+ :project-name="itemName"
+ aria-hidden="true"
+ />
<div
- v-if="namespace"
- data-testid="frequent-items-item-namespace"
- :title="namespace"
- class="frequent-items-item-namespace"
+ data-testid="frequent-items-item-metadata-container"
+ class="frequent-items-item-metadata-container"
>
- {{ truncatedNamespace }}
+ <div
+ v-safe-html="highlightedItemName"
+ data-testid="frequent-items-item-title"
+ :title="itemName"
+ class="frequent-items-item-title"
+ ></div>
+ <div
+ v-if="namespace"
+ data-testid="frequent-items-item-namespace"
+ :title="namespace"
+ class="frequent-items-item-namespace"
+ >
+ {{ truncatedNamespace }}
+ </div>
</div>
</div>
</gl-button>
+ <gl-button
+ v-if="isItemsListEditable"
+ v-gl-tooltip.left
+ size="small"
+ category="tertiary"
+ :aria-label="__('Remove')"
+ :title="__('Remove')"
+ class="gl-align-self-center gl-p-1! gl-absolute! gl-w-auto! gl-top-4 gl-right-4"
+ data-testid="item-remove"
+ @click.stop.prevent="removeFrequentItem(itemId)"
+ >
+ <gl-icon name="close" />
+ </gl-button>
</li>
</template>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
index cb5d21161a9..a7c27abf58e 100644
--- a/app/assets/javascripts/frequent_items/constants.js
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -18,6 +18,7 @@ export const TRANSLATION_KEYS = {
projects: {
loadingMessage: s__('ProjectsDropdown|Loading projects'),
header: s__('ProjectsDropdown|Frequently visited'),
+ headerEditToggle: s__('ProjectsDropdown|Toggle edit mode'),
itemListErrorMessage: s__(
'ProjectsDropdown|This feature requires browser localStorage support',
),
@@ -29,6 +30,7 @@ export const TRANSLATION_KEYS = {
groups: {
loadingMessage: s__('GroupsDropdown|Loading groups'),
header: s__('GroupsDropdown|Frequently visited'),
+ headerEditToggle: s__('GroupsDropdown|Toggle edit mode'),
itemListErrorMessage: s__('GroupsDropdown|This feature requires browser localStorage support'),
itemListEmptyMessage: s__('GroupsDropdown|Groups you visit often will appear here'),
searchListErrorMessage: s__('GroupsDropdown|Something went wrong on our end.'),
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index babc2ef2e32..e5ef49ec402 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -12,6 +12,10 @@ export const setStorageKey = ({ commit }, key) => {
commit(types.SET_STORAGE_KEY, key);
};
+export const toggleItemsListEditablity = ({ commit }) => {
+ commit(types.TOGGLE_ITEMS_LIST_EDITABILITY);
+};
+
export const requestFrequentItems = ({ commit }) => {
commit(types.REQUEST_FREQUENT_ITEMS);
};
@@ -81,3 +85,28 @@ export const setSearchQuery = ({ commit, dispatch }, query) => {
dispatch('fetchFrequentItems');
}
};
+
+export const removeFrequentItemSuccess = ({ commit }, itemId) => {
+ commit(types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS, itemId);
+};
+
+export const removeFrequentItemError = ({ commit }) => {
+ commit(types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR);
+};
+
+export const removeFrequentItem = ({ state, dispatch }, itemId) => {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ try {
+ const storedRawItems = JSON.parse(localStorage.getItem(state.storageKey));
+ localStorage.setItem(
+ state.storageKey,
+ JSON.stringify(storedRawItems.filter((item) => item.id !== itemId)),
+ );
+ dispatch('removeFrequentItemSuccess', itemId);
+ } catch {
+ dispatch('removeFrequentItemError');
+ }
+ } else {
+ dispatch('removeFrequentItemError');
+ }
+};
diff --git a/app/assets/javascripts/frequent_items/store/mutation_types.js b/app/assets/javascripts/frequent_items/store/mutation_types.js
index cbe2c9401ad..9c9346081e9 100644
--- a/app/assets/javascripts/frequent_items/store/mutation_types.js
+++ b/app/assets/javascripts/frequent_items/store/mutation_types.js
@@ -1,9 +1,12 @@
export const SET_NAMESPACE = 'SET_NAMESPACE';
export const SET_STORAGE_KEY = 'SET_STORAGE_KEY';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
+export const TOGGLE_ITEMS_LIST_EDITABILITY = 'TOGGLE_ITEMS_LIST_EDITABILITY';
export const REQUEST_FREQUENT_ITEMS = 'REQUEST_FREQUENT_ITEMS';
export const RECEIVE_FREQUENT_ITEMS_SUCCESS = 'RECEIVE_FREQUENT_ITEMS_SUCCESS';
export const RECEIVE_FREQUENT_ITEMS_ERROR = 'RECEIVE_FREQUENT_ITEMS_ERROR';
export const REQUEST_SEARCHED_ITEMS = 'REQUEST_SEARCHED_ITEMS';
export const RECEIVE_SEARCHED_ITEMS_SUCCESS = 'RECEIVE_SEARCHED_ITEMS_SUCCESS';
export const RECEIVE_SEARCHED_ITEMS_ERROR = 'RECEIVE_SEARCHED_ITEMS_ERROR';
+export const RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS = 'RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS';
+export const RECEIVE_REMOVE_FREQUENT_ITEM_ERROR = 'RECEIVE_REMOVE_FREQUENT_ITEM_ERROR';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
index eee00243867..65f54e6ed05 100644
--- a/app/assets/javascripts/frequent_items/store/mutations.js
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -20,6 +20,11 @@ export default {
hasSearchQuery,
});
},
+ [types.TOGGLE_ITEMS_LIST_EDITABILITY](state) {
+ Object.assign(state, {
+ isItemsListEditable: !state.isItemsListEditable,
+ });
+ },
[types.REQUEST_FREQUENT_ITEMS](state) {
Object.assign(state, {
isLoadingItems: true,
@@ -69,4 +74,15 @@ export default {
isFetchFailed: true,
});
},
+ [types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS](state, itemId) {
+ Object.assign(state, {
+ items: state.items.filter((item) => item.id !== itemId),
+ isItemRemovalFailed: false,
+ });
+ },
+ [types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR](state) {
+ Object.assign(state, {
+ isItemRemovalFailed: true,
+ });
+ },
};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
index c5c0b25fdf2..ee94e9cd221 100644
--- a/app/assets/javascripts/frequent_items/store/state.js
+++ b/app/assets/javascripts/frequent_items/store/state.js
@@ -5,5 +5,7 @@ export default ({ dropdownType = '' } = {}) => ({
searchQuery: '',
isLoadingItems: false,
isFetchFailed: false,
+ isItemsListEditable: false,
+ isItemRemovalFailed: false,
items: [],
});
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 293cd2df16f..81da8409873 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -39,8 +39,18 @@ export const CONTACTS_REMOVE_COMMAND = '/remove_contacts';
* @param string user input
* @return {string} escaped user input
*/
-function escape(string) {
- return lodashEscape(string).replace(/\$/g, '&dollar;');
+export function escape(string) {
+ // To prevent double (or multiple) enconding attack
+ // Decode the user input repeatedly prior to escaping the final decoded string.
+ let encodedString = string;
+ let decodedString = decodeURIComponent(encodedString);
+
+ while (decodedString !== encodedString) {
+ encodedString = decodeURIComponent(decodedString);
+ decodedString = decodeURIComponent(encodedString);
+ }
+
+ return lodashEscape(decodedString.replace(/\$/g, '&dollar;'));
}
export function showAndHideHelper($input, alias = '') {
@@ -106,6 +116,7 @@ export const defaultAutocompleteConfig = {
issues: true,
mergeRequests: true,
epics: true,
+ iterations: true,
milestones: true,
labels: true,
snippets: true,
@@ -209,6 +220,10 @@ class GfmAutoComplete {
[[referencePrefix]] = value.params;
if (/^[@%~]/.test(referencePrefix)) {
tpl += '<%- referencePrefix %>';
+ } else if (/^[*]/.test(referencePrefix)) {
+ // EE-ONLY
+ referencePrefix = '*iteration:';
+ tpl += '<%- referencePrefix %>';
}
}
}
@@ -883,7 +898,8 @@ class GfmAutoComplete {
const atSymbolsWithBar = Object.keys(controllers)
.join('|')
.replace(/[$]/, '\\$&')
- .replace(/([[\]:])/g, '\\$1');
+ .replace(/([[\]:])/g, '\\$1')
+ .replace(/([*])/g, '\\$1');
const atSymbolsWithoutBar = Object.keys(controllers).join('');
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
@@ -912,6 +928,7 @@ GfmAutoComplete.atTypeMap = {
'#': 'issues',
'!': 'mergeRequests',
'&': 'epics',
+ '*iteration:': 'iterations',
'~': 'labels',
'%': 'milestones',
'/': 'commands',
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index 15e7ef7d62c..01cc2fc3018 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -5,6 +5,7 @@ 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_NOTES } from '~/work_items/constants';
export const config = {
typeDefs,
@@ -22,10 +23,30 @@ export const config = {
},
},
},
+ WorkItemWidgetNotes: {
+ fields: {
+ // If we add any key args, the discussions field becomes discussions({"filter":"ONLY_ACTIVITY","first":10}) and
+ // kills any possibility to handle it on the widget level without hardcoding a string.
+ discussions: {
+ keyArgs: false,
+ },
+ },
+ },
+ WorkItemWidgetProgress: {
+ fields: {
+ progress: {
+ // We want to show null progress as 0 as per https://gitlab.com/gitlab-org/gitlab/-/issues/386117
+ read(existing) {
+ return existing === null ? 0 : existing;
+ },
+ },
+ },
+ },
WorkItem: {
fields: {
+ // widgets policy because otherwise the subscriptions invalidate the cache
widgets: {
- merge(existing = [], incoming) {
+ merge(existing = [], incoming, context) {
if (existing.length === 0) {
return incoming;
}
@@ -33,6 +54,24 @@ export const config = {
const incomingWidget = incoming.find(
(w) => w.type && w.type === existingWidget.type,
);
+ // We don't want to override existing notes with empty widget on work item updates
+ if (incomingWidget?.type === WIDGET_TYPE_NOTES && !context.variables.pageSize) {
+ return existingWidget;
+ }
+ // we want to concat next page of discussions to the existing ones
+ if (incomingWidget?.type === WIDGET_TYPE_NOTES && context.variables.after) {
+ // concatPagination won't work because we were placing new widget here so we have to do this manually
+ return {
+ ...incomingWidget,
+ discussions: {
+ ...incomingWidget.discussions,
+ nodes: [
+ ...existingWidget.discussions.nodes,
+ ...incomingWidget.discussions.nodes,
+ ],
+ },
+ };
+ }
return incomingWidget || existingWidget;
});
},
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 5467105ac3c..a622b342c0a 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -153,8 +153,9 @@
"WorkItemWidgetMilestone",
"WorkItemWidgetNotes",
"WorkItemWidgetProgress",
+ "WorkItemWidgetRequirementLegacy",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetStatus",
"WorkItemWidgetWeight"
]
-} \ No newline at end of file
+}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
deleted file mode 100644
index fb0c47fe018..00000000000
--- a/app/assets/javascripts/groups_select.js
+++ /dev/null
@@ -1,122 +0,0 @@
-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(() => {
- // Needs to be accessible in rspec
- window.GROUP_SELECT_PER_PAGE = 20;
-
- $('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
- const $select = $(this);
- const allAvailable = $select.data('allAvailable');
- const skipGroups = $select.data('skipGroups') || [];
- const parentGroupID = $select.data('parentId');
- const groupsFilter = $select.data('groupsFilter');
- const minAccessLevel = $select.data('minAccessLevel');
-
- $select.select2({
- placeholder: __('Search for a group'),
- allowClear: $select.hasClass('allowClear'),
- multiple: $select.hasClass('multiselect'),
- minimumInputLength: 0,
- ajax: {
- url: Api.buildUrl(groupsPath(groupsFilter, parentGroupID)),
- dataType: 'json',
- quietMillis: 250,
- transport: select2AxiosTransport,
- data(search, page) {
- return {
- search,
- page,
- per_page: window.GROUP_SELECT_PER_PAGE,
- all_available: allAvailable,
- min_access_level: minAccessLevel,
- };
- },
- results(data, page) {
- const groups = data.length ? data : data.results || [];
- const more = data.pagination ? data.pagination.more : false;
- const results = groups.filter((group) => skipGroups.indexOf(group.id) === -1);
-
- return {
- results,
- page,
- more,
- };
- },
- },
- // eslint-disable-next-line consistent-return
- initSelection(element, callback) {
- const id = $(element).val();
- if (id !== '') {
- return Api.group(id, callback);
- }
- },
- formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${escape(
- object.full_name,
- )}</div> <div class='group-path'>${object.full_path}</div> </div>`;
- },
- formatSelection(object) {
- return escape(object.full_name);
- },
- dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
- // we do not want to escape markup since we are displaying html in results
- escapeMarkup(m) {
- return m;
- },
- });
-
- $select.on('select2-loaded', () => {
- const dropdown = document.querySelector('.select2-infinite .select2-results');
- dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
- });
- });
- })
- .catch(() => {});
-};
-
-export default () => {
- if ($('.ajax-groups-select').length) {
- if (gon.features?.vueGroupSelect) {
- initVueSelect();
- } else {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(groupsSelect)
- .catch(() => {});
- }
- }
-};
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 3c9c0b1ade1..b95f8bb5acb 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -27,7 +27,6 @@ import { performanceMarkAndMeasure } from '~/performance/utils';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
leftSidebarViews,
viewerTypes,
@@ -55,7 +54,6 @@ export default {
DiffViewer,
FileTemplatesBar,
},
- mixins: [glFeatureFlagMixin()],
props: {
file: {
type: Object,
@@ -474,7 +472,7 @@ export default {
this.editor.registerCiSchema();
};
- if (this.isCiConfigFile && this.glFeatures.schemaLinting) {
+ if (this.isCiConfigFile) {
registerLocalSchema();
} else {
if (this.CiSchemaExtension) {
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
index fbd2ce4ce69..dbb68b7facd 100644
--- a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
@@ -1,7 +1,12 @@
-import { cleanEndingSeparator } from '~/lib/utils/url_utility';
+import { cleanEndingSeparator, joinPaths } from '~/lib/utils/url_utility';
const getBaseUrl = () => {
- const baseUrlObj = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
+ const path = joinPaths(
+ '/',
+ window.gon.relative_url_root || '',
+ process.env.GITLAB_WEB_IDE_PUBLIC_PATH,
+ );
+ const baseUrlObj = new URL(path, window.location.origin);
return cleanEndingSeparator(baseUrlObj.href);
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index cbc6e0fe519..d490b8c5dad 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -183,7 +183,11 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
dispatch(
'redirectToUrl',
- createNewMergeRequestUrl(currentProject.web_url, branchName, targetBranch),
+ createNewMergeRequestUrl(
+ currentProject.web_url,
+ encodeURIComponent(branchName),
+ encodeURIComponent(targetBranch),
+ ),
{ root: true },
);
}
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 62476b7fc63..6eb56a68429 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -1,6 +1,6 @@
import axios from 'axios';
import Visibility from 'visibilityjs';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import { rightSidebarViews } from '../../../constants';
@@ -24,7 +24,7 @@ export const forcePipelineRequest = () => {
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
export const receiveLatestPipelineError = ({ commit, dispatch }, err) => {
- if (err.status !== httpStatus.NOT_FOUND) {
+ if (err.status !== HTTP_STATUS_NOT_FOUND) {
dispatch(
'setErrorMessage',
{
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js
index 91645a34a3d..c4198a7427f 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../constants';
import * as messages from '../messages';
@@ -18,7 +18,7 @@ export const receiveConfigCheckError = ({ commit, state }, e) => {
const { status } = e.response;
const { paths } = state;
- const isVisible = status !== httpStatus.FORBIDDEN && status !== httpStatus.NOT_FOUND;
+ const isVisible = status !== HTTP_STATUS_FORBIDDEN && status !== HTTP_STATUS_NOT_FOUND;
commit(types.SET_VISIBLE, isVisible);
const message = messages.configCheckError(status, paths.webTerminalConfigHelpPath);
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
index a510ec0847b..874cc5094d3 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
@@ -1,6 +1,6 @@
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
import { STARTING, STOPPING, STOPPED } from '../constants';
import * as messages from '../messages';
@@ -107,7 +107,7 @@ export const restartSession = ({ state, dispatch, rootState }) => {
const responseStatus = error.response && error.response.status;
// We may have removed the build, in this case we'll just create a new session
if (
- responseStatus === httpStatus.NOT_FOUND ||
+ responseStatus === HTTP_STATUS_NOT_FOUND ||
responseStatus === HTTP_STATUS_UNPROCESSABLE_ENTITY
) {
dispatch('startSession');
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index fa1c7f23677..ad7ad35a98c 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { __, sprintf } from '~/locale';
export const UNEXPECTED_ERROR_CONFIG = __(
@@ -39,7 +39,7 @@ export const configCheckError = (status, helpUrl) => {
},
false,
);
- } else if (status === httpStatus.FORBIDDEN) {
+ } else if (status === HTTP_STATUS_FORBIDDEN) {
return ERROR_PERMISSION;
}
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index bd69165f0ca..db677c574d1 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -7,13 +7,21 @@ import { STATUSES } from '../constants';
const STATISTIC_ITEMS = {
diff_note: __('Diff notes'),
issue: __('Issues'),
+ issue_attachment: s__('GithubImporter|Issue attachments'),
+ issue_event: __('Issue events'),
label: __('Labels'),
+ lfs_object: __('LFS objects'),
+ merge_request_attachment: s__('GithubImporter|Merge request attachments'),
milestone: __('Milestones'),
note: __('Notes'),
+ note_attachment: s__('GithubImporter|Note attachments'),
+ protected_branch: __('Protected branches'),
pull_request: s__('GithubImporter|Pull requests'),
pull_request_merged_by: s__('GithubImporter|PR mergers'),
pull_request_review: s__('GithubImporter|PR reviews'),
+ pull_request_review_request: s__('GithubImporter|PR reviews'),
release: __('Releases'),
+ release_attachment: s__('GithubImporter|Release attachments'),
};
// support both camel case and snake case versions
@@ -93,18 +101,17 @@ export default {
mappedStatus() {
if (this.status === STATUSES.FINISHED) {
const isIncomplete = this.stats && isIncompleteImport(this.stats);
- return {
- icon: 'status-success',
- ...(isIncomplete
- ? {
- text: __('Partial import'),
- variant: 'warning',
- }
- : {
- text: __('Complete'),
- variant: 'success',
- }),
- };
+ return isIncomplete
+ ? {
+ icon: 'status-alert',
+ text: __('Partial import'),
+ variant: 'warning',
+ }
+ : {
+ icon: 'status-success',
+ text: __('Complete'),
+ variant: 'success',
+ };
}
return STATUS_MAP[this.status];
@@ -120,6 +127,8 @@ export default {
return { name: 'status-success', class: 'gl-text-green-400' };
} else if (imported === 0) {
return { name: 'status-scheduled', class: 'gl-text-gray-400' };
+ } else if (this.status === STATUSES.FINISHED) {
+ return { name: 'status-alert', class: 'gl-text-orange-400' };
}
return { name: 'status-running', class: 'gl-text-blue-400' };
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
index deaf2654424..8d72942447c 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
@@ -1,15 +1,27 @@
<script>
-import { GlButton, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlIcon,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
export default {
components: {
GlIcon,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
directives: {
GlTooltip,
},
props: {
+ isProjectsImportEnabled: {
+ type: Boolean,
+ required: true,
+ },
isFinished: {
type: Boolean,
required: true,
@@ -23,13 +35,32 @@ export default {
required: true,
},
},
+ methods: {
+ importGroup(extraArgs = {}) {
+ this.$emit('import-group', extraArgs);
+ },
+ },
};
</script>
<template>
<span class="gl-white-space-nowrap gl-inline-flex gl-align-items-center">
+ <gl-dropdown
+ v-if="isProjectsImportEnabled && isAvailableForImport"
+ :text="isFinished ? __('Re-import with projects') : __('Import with projects')"
+ :disabled="isInvalid"
+ variant="confirm"
+ category="secondary"
+ data-qa-selector="import_group_button"
+ split
+ @click="importGroup({ migrateProjects: true })"
+ >
+ <gl-dropdown-item @click="importGroup({ migrateProjects: false })">{{
+ isFinished ? __('Re-import without projects') : __('Import without projects')
+ }}</gl-dropdown-item>
+ </gl-dropdown>
<gl-button
- v-if="isAvailableForImport"
+ v-else-if="isAvailableForImport"
:disabled="isInvalid"
variant="confirm"
category="secondary"
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 6412f26fde7..c590d832568 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
@@ -2,6 +2,8 @@
import {
GlAlert,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlEmptyState,
GlIcon,
GlLink,
@@ -15,6 +17,7 @@ import {
import { debounce } from 'lodash';
import { createAlert } from '~/flash';
import { s__, __, n__, sprintf } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { getGroupPathAvailability } from '~/rest_api';
@@ -47,6 +50,8 @@ export default {
components: {
GlAlert,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlEmptyState,
GlIcon,
GlLink,
@@ -65,6 +70,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
sourceUrl: {
type: String,
@@ -128,32 +134,36 @@ export default {
{
key: 'webUrl',
label: s__('BulkImport|Source group'),
- thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! import-jobs-from-col`,
+ thClass: `${DEFAULT_TH_CLASSES} gl-pl-0! gl-w-half`,
// eslint-disable-next-line @gitlab/require-i18n-strings
tdClass: `${DEFAULT_TD_CLASSES} gl-pl-0!`,
},
{
key: 'importTarget',
label: s__('BulkImport|New group'),
- thClass: `${DEFAULT_TH_CLASSES} import-jobs-to-col`,
+ thClass: `${DEFAULT_TH_CLASSES} gl-w-half`,
tdClass: DEFAULT_TD_CLASSES,
},
{
key: 'progress',
label: __('Status'),
- thClass: `${DEFAULT_TH_CLASSES} import-jobs-status-col`,
+ thClass: `${DEFAULT_TH_CLASSES}`,
tdClass: DEFAULT_TD_CLASSES,
tdAttr: { 'data-qa-selector': 'import_status_indicator' },
},
{
key: 'actions',
label: '',
- thClass: `${DEFAULT_TH_CLASSES} import-jobs-cta-col`,
+ thClass: `${DEFAULT_TH_CLASSES}`,
tdClass: DEFAULT_TD_CLASSES,
},
],
computed: {
+ isProjectsImportEnabled() {
+ return Boolean(this.glFeatures.bulkImportProjects);
+ },
+
groups() {
return this.bulkImportSourceGroups?.nodes ?? [];
},
@@ -260,7 +270,7 @@ export default {
const table = this.getTableRef();
const matches = new Set();
this.groups.forEach((g, idx) => {
- if (!this.importGroups[g.id]) {
+ if (!this.importTargets[g.id]) {
this.setDefaultImportTarget(g);
}
@@ -375,13 +385,14 @@ export default {
}
},
- importSelectedGroups() {
+ importSelectedGroups(extraArgs = {}) {
const importRequests = this.groupsTableData
.filter((group) => this.selectedGroupsIds.includes(group.id))
.map((group) => ({
sourceGroupId: group.id,
targetNamespace: group.importTarget.targetNamespace.fullPath,
newName: group.importTarget.newName,
+ ...extraArgs,
}));
this.importGroups(importRequests);
@@ -521,6 +532,7 @@ export default {
gitlabLogo: window.gon.gitlab_logo,
PAGE_SIZES,
permissionsHelpPath: helpPagePath('user/permissions', { anchor: 'group-members-permissions' }),
+ betaFeatureHelpPath: helpPagePath('policy/alpha-beta-support', { anchor: 'beta-features' }),
popoverOptions: { title: __('What is listed here?') },
i18n,
LOCAL_STORAGE_KEY: 'gl-bulk-imports-status-page-size-v1',
@@ -637,7 +649,7 @@ export default {
</gl-empty-state>
<template v-else>
<div
- class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-px-4 gl-display-flex gl-align-items-center import-table-bar"
+ class="gl-bg-gray-10 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-px-4 gl-display-flex gl-align-items-center gl-sticky gl-z-index-3 import-table-bar"
>
<span data-test-id="selection-count">
<gl-sprintf :message="__('%{count} selected')">
@@ -646,7 +658,22 @@ export default {
</template>
</gl-sprintf>
</span>
+ <gl-dropdown
+ v-if="isProjectsImportEnabled"
+ :text="s__('BulkImport|Import with projects')"
+ :disabled="!hasSelectedGroups"
+ variant="confirm"
+ category="primary"
+ class="gl-ml-4"
+ split
+ @click="importSelectedGroups({ migrateProjects: true })"
+ >
+ <gl-dropdown-item @click="importSelectedGroups({ migrateProjects: false })">
+ {{ s__('BulkImport|Import without projects') }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<gl-button
+ v-else
category="primary"
variant="confirm"
class="gl-ml-4"
@@ -654,6 +681,22 @@ export default {
@click="importSelectedGroups"
>{{ s__('BulkImport|Import selected') }}</gl-button
>
+ <span class="gl-ml-3">
+ <gl-icon name="information-o" :size="12" class="gl-text-blue-600" />
+ <gl-sprintf
+ :message="
+ s__(
+ 'BulkImport|Importing projects is a %{docsLinkStart}Beta%{docsLinkEnd} feature.',
+ )
+ "
+ >
+ <template #docsLink="{ content }"
+ ><gl-link :href="$options.betaFeatureHelpPath" target="_blank">{{
+ content
+ }}</gl-link></template
+ >
+ </gl-sprintf>
+ </span>
</div>
<gl-table
ref="table"
@@ -661,6 +704,7 @@ export default {
data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
+ thead-class="gl-sticky gl-z-index-2 gl-bg-gray-10"
:items="groupsTableData"
:fields="$options.fields"
selectable
@@ -711,6 +755,7 @@ export default {
</template>
<template #cell(actions)="{ item: group }">
<import-actions-cell
+ :is-projects-import-enabled="isProjectsImportEnabled"
:is-finished="group.flags.isFinished"
:is-available-for-import="group.flags.isAvailableForImport"
:is-invalid="group.flags.isInvalid"
@@ -720,6 +765,7 @@ export default {
sourceGroupId: group.id,
targetNamespace: group.importTarget.targetNamespace.fullPath,
newName: group.importTarget.newName,
+ ...$event,
},
])
"
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index 913a5a659b3..de0595360bf 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -153,6 +153,7 @@ export function createResolvers({ endpoints }) {
source_full_path: op.group.fullPath,
destination_namespace: op.targetNamespace,
destination_name: op.newName,
+ migrate_projects: op.migrateProjects,
})),
});
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
index c48e22a7717..83d17a5baa7 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -74,6 +74,7 @@ input ImportRequestInput {
sourceGroupId: ID!
targetNamespace: String!
newName: String!
+ migrateProjects: Boolean!
}
extend type Mutation {
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index b8faf349375..da5dcfa71e3 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -140,7 +140,7 @@ export default {
>
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted">
- <div class="import-entities-target-select gl-display-flex gl-align-items-stretch gl-w-full">
+ <div class="gl-display-flex gl-align-items-stretch gl-w-full">
<import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace">
<template v-if="namespaces.length">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
@@ -161,7 +161,7 @@ export default {
}}</gl-dropdown-item>
</import-group-dropdown>
<div
- class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
+ class="gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
>
/
</div>
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 197fb03af2c..485511510f7 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -57,7 +57,11 @@ const apolloProvider = new VueApollo({
defaultClient,
});
-export default function mountImportProjectsTable(mountElement) {
+export default function mountImportProjectsTable({
+ mountElement,
+ Component = ImportProjectsTable,
+ extraProps = () => ({}),
+}) {
if (!mountElement) return undefined;
const store = initStoreFromElement(mountElement);
@@ -68,7 +72,7 @@ export default function mountImportProjectsTable(mountElement) {
store,
apolloProvider,
render(createElement) {
- return createElement(ImportProjectsTable, { props });
+ return createElement(Component, { props: { ...props, ...extraProps(mountElement.dataset) } });
},
});
}
diff --git a/app/assets/javascripts/init_diff_stats_dropdown.js b/app/assets/javascripts/init_diff_stats_dropdown.js
index 27df761a103..8413fe92f89 100644
--- a/app/assets/javascripts/init_diff_stats_dropdown.js
+++ b/app/assets/javascripts/init_diff_stats_dropdown.js
@@ -4,7 +4,7 @@ import { stickyMonitor } from './lib/utils/sticky';
export const initDiffStatsDropdown = (stickyTop) => {
if (stickyTop) {
- stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
+ stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop, false);
}
const el = document.querySelector('.js-diff-stats-dropdown');
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 392dd63b089..b956bdf067d 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -52,6 +52,7 @@ export const integrationTriggerEvents = {
TAG_PUSH: 'tag_push_events',
PIPELINE: 'pipeline_events',
WIKI_PAGE: 'wiki_page_events',
+ DEPLOYMENT: 'deployment_events',
};
export const integrationTriggerEventTitles = {
@@ -72,6 +73,9 @@ export const integrationTriggerEventTitles = {
[integrationTriggerEvents.TAG_PUSH]: s__('IntegrationEvents|A tag is pushed to the repository'),
[integrationTriggerEvents.PIPELINE]: s__('IntegrationEvents|A pipeline status changes'),
[integrationTriggerEvents.WIKI_PAGE]: s__('IntegrationEvents|A wiki page is created or updated'),
+ [integrationTriggerEvents.DEPLOYMENT]: s__(
+ 'IntegrationEvents|A deployment is started or finished',
+ ),
};
export const billingPlans = {
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index d86e6326f64..1e58b604bf7 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlBadge, GlButton, GlForm } from '@gitlab/ui';
+import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
@@ -10,8 +10,6 @@ import {
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
INTEGRATION_FORM_TYPE_SLACK,
- integrationFormSectionComponents,
- billingPlanNames,
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
@@ -21,6 +19,7 @@ import ActiveCheckbox from './active_checkbox.vue';
import DynamicField from './dynamic_field.vue';
import OverrideDropdown from './override_dropdown.vue';
import TriggerFields from './trigger_fields.vue';
+import IntegrationFormSection from './integration_forms/section.vue';
import IntegrationFormActions from './integration_form_actions.vue';
export default {
@@ -31,29 +30,8 @@ export default {
TriggerFields,
DynamicField,
IntegrationFormActions,
- IntegrationSectionConfiguration: () =>
- import(
- /* webpackChunkName: 'integrationSectionConfiguration' */ '~/integrations/edit/components/sections/configuration.vue'
- ),
- IntegrationSectionConnection: () =>
- import(
- /* webpackChunkName: 'integrationSectionConnection' */ '~/integrations/edit/components/sections/connection.vue'
- ),
- IntegrationSectionJiraIssues: () =>
- import(
- /* webpackChunkName: 'integrationSectionJiraIssues' */ '~/integrations/edit/components/sections/jira_issues.vue'
- ),
- IntegrationSectionJiraTrigger: () =>
- import(
- /* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
- ),
- IntegrationSectionTrigger: () =>
- import(
- /* webpackChunkName: 'integrationSectionTrigger' */ '~/integrations/edit/components/sections/trigger.vue'
- ),
+ IntegrationFormSection,
GlAlert,
- GlBadge,
- GlButton,
GlForm,
},
directives: {
@@ -120,9 +98,6 @@ export default {
},
methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']),
- fieldsForSection(section) {
- return this.propsSource.fields.filter((field) => field.section === section.type);
- },
form() {
return this.$refs.integrationForm.$el;
},
@@ -189,23 +164,21 @@ export default {
this.isResetting = false;
});
},
- onRequestJiraIssueTypes() {
- this.requestJiraIssueTypes(this.getFormData());
- },
getFormData() {
return new FormData(this.form());
},
onToggleIntegrationState(integrationActive) {
this.integrationActive = integrationActive;
},
+ onRequestJiraIssueTypes() {
+ this.requestJiraIssueTypes(this.getFormData());
+ },
},
helpHtmlConfig: {
ADD_TAGS: ['use'], // to support icon SVGs
FORBID_ATTR: [], // This is trusted input so we can override the default config to allow data-* attributes
},
csrf,
- integrationFormSectionComponents,
- billingPlanNames,
slackUpgradeInfo: {
title: s__(
`SlackIntegration|Update to the latest version of GitLab for Slack to get notifications`,
@@ -280,42 +253,15 @@ export default {
</div>
<template v-if="hasSections">
- <div
+ <integration-form-section
v-for="(section, index) in customState.sections"
:key="section.type"
+ :section="section"
+ :is-validated="isValidated"
:class="{ 'gl-border-b gl-pb-3 gl-mb-6': index !== customState.sections.length - 1 }"
- data-testid="integration-section"
- >
- <section class="gl-lg-display-flex">
- <div class="gl-flex-basis-third gl-mr-4">
- <h4 class="gl-mt-0">
- {{ section.title
- }}<gl-badge
- v-if="section.plan"
- :href="propsSource.aboutPricingUrl"
- target="_blank"
- rel="noopener noreferrer"
- variant="tier"
- icon="license"
- class="gl-ml-3"
- >
- {{ $options.billingPlanNames[section.plan] }}
- </gl-badge>
- </h4>
- <p v-safe-html="section.description"></p>
- </div>
-
- <div class="gl-flex-basis-two-thirds">
- <component
- :is="$options.integrationFormSectionComponents[section.type]"
- :fields="fieldsForSection(section)"
- :is-validated="isValidated"
- @toggle-integration-active="onToggleIntegrationState"
- @request-jira-issue-types="onRequestJiraIssueTypes"
- />
- </div>
- </section>
- </div>
+ @toggle-integration-active="onToggleIntegrationState"
+ @request-jira-issue-types="onRequestJiraIssueTypes"
+ />
</template>
<section v-if="hasFieldsWithoutSection" class="gl-lg-display-flex gl-justify-content-end">
diff --git a/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue b/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue
new file mode 100644
index 00000000000..ce39954735a
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/integration_forms/section.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { integrationFormSectionComponents, billingPlanNames } from '~/integrations/constants';
+
+export default {
+ name: 'IntegrationFormSection',
+ components: {
+ GlBadge,
+ IntegrationSectionConfiguration: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionConfiguration' */ '~/integrations/edit/components/sections/configuration.vue'
+ ),
+ IntegrationSectionConnection: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionConnection' */ '~/integrations/edit/components/sections/connection.vue'
+ ),
+ IntegrationSectionJiraIssues: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionJiraIssues' */ '~/integrations/edit/components/sections/jira_issues.vue'
+ ),
+ IntegrationSectionJiraTrigger: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionJiraTrigger' */ '~/integrations/edit/components/sections/jira_trigger.vue'
+ ),
+ IntegrationSectionTrigger: () =>
+ import(
+ /* webpackChunkName: 'integrationSectionTrigger' */ '~/integrations/edit/components/sections/trigger.vue'
+ ),
+ },
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ section: {
+ type: Object,
+ required: true,
+ },
+ isValidated: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapGetters(['propsSource']),
+ },
+ methods: {
+ fieldsForSection(section) {
+ return this.propsSource.fields.filter((field) => field.section === section.type);
+ },
+ },
+ billingPlanNames,
+ integrationFormSectionComponents,
+};
+</script>
+<template>
+ <section class="gl-lg-display-flex">
+ <div class="gl-flex-basis-third gl-mr-4">
+ <h4 class="gl-mt-0">
+ {{ section.title
+ }}<gl-badge
+ v-if="section.plan"
+ :href="propsSource.aboutPricingUrl"
+ target="_blank"
+ rel="noopener noreferrer"
+ variant="tier"
+ icon="license"
+ class="gl-ml-3"
+ >
+ {{ $options.billingPlanNames[section.plan] }}
+ </gl-badge>
+ </h4>
+ <p v-safe-html="section.description"></p>
+ </div>
+
+ <div
+ v-if="$options.integrationFormSectionComponents[section.type]"
+ class="gl-flex-basis-two-thirds"
+ >
+ <component
+ :is="$options.integrationFormSectionComponents[section.type]"
+ :fields="fieldsForSection(section)"
+ :is-validated="isValidated"
+ @toggle-integration-active="$emit('toggle-integration-active', $event)"
+ @request-jira-issue-types="$emit('request-jira-issue-types', $event)"
+ />
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/components/sections/trigger.vue b/app/assets/javascripts/integrations/edit/components/sections/trigger.vue
index 9af5070d4cf..00546671aa7 100644
--- a/app/assets/javascripts/integrations/edit/components/sections/trigger.vue
+++ b/app/assets/javascripts/integrations/edit/components/sections/trigger.vue
@@ -15,11 +15,12 @@ export default {
</script>
<template>
- <div>
+ <div data-testid="trigger-fields-group">
<trigger-field
v-for="event in propsSource.triggerEvents"
:key="`${currentKey}-trigger-fields-${event.name}`"
:event="event"
+ :type="propsSource.type"
class="gl-mb-3"
/>
</div>
diff --git a/app/assets/javascripts/integrations/edit/components/trigger_field.vue b/app/assets/javascripts/integrations/edit/components/trigger_field.vue
index dc5ae2f3a3d..57753c61587 100644
--- a/app/assets/javascripts/integrations/edit/components/trigger_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/trigger_field.vue
@@ -1,13 +1,17 @@
<script>
-import { GlFormCheckbox } from '@gitlab/ui';
+import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import { integrationTriggerEventTitles } from '~/integrations/constants';
+import {
+ placeholderForType,
+ integrationTriggerEventTitles,
+} from 'any_else_ce/integrations/constants';
export default {
name: 'TriggerField',
components: {
GlFormCheckbox,
+ GlFormInput,
},
props: {
event: {
@@ -15,10 +19,15 @@ export default {
required: false,
default: () => ({}),
},
+ type: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
value: false,
+ fieldValue: this.event.field?.value,
};
},
computed: {
@@ -26,9 +35,15 @@ export default {
name() {
return `service[${this.event.name}]`;
},
+ fieldName() {
+ return `service[${this.event.field?.name}]`;
+ },
title() {
return integrationTriggerEventTitles[this.event.name];
},
+ defaultPlaceholder() {
+ return placeholderForType[this.type];
+ },
},
mounted() {
this.value = this.event.value || false;
@@ -42,5 +57,15 @@ export default {
<gl-form-checkbox v-model="value" :disabled="isInheriting">
{{ title }}
</gl-form-checkbox>
+ <div class="gl-ml-6">
+ <gl-form-input
+ v-if="event.field"
+ v-show="value"
+ v-model="fieldValue"
+ :name="fieldName"
+ :placeholder="event.field.placeholder || defaultPlaceholder"
+ :readonly="isInheriting"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index fbb547c28ff..fa1aa6b0d88 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -18,8 +18,6 @@ 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_VARIANT,
- REACHED_LIMIT_VARIANT,
USERS_FILTER_ALL,
INVITE_MEMBERS_FOR_TASK,
MEMBER_MODAL_LABELS,
@@ -189,10 +187,10 @@ export default {
return this.source === LEARN_GITLAB;
},
showUserLimitNotification() {
- return this.usersLimitDataset.reachedLimit || this.usersLimitDataset.closeToDashboardLimit;
+ return !isEmpty(this.usersLimitDataset.alertVariant);
},
limitVariant() {
- return this.usersLimitDataset.reachedLimit ? REACHED_LIMIT_VARIANT : CLOSE_TO_LIMIT_VARIANT;
+ return this.usersLimitDataset.alertVariant;
},
errorList() {
return Object.entries(this.invalidMembers).map(([member, error]) => {
@@ -479,6 +477,7 @@ export default {
</gl-alert>
<user-limit-notification
v-else-if="showUserLimitNotification"
+ class="gl-mb-5"
:limit-variant="limitVariant"
:users-limit-dataset="usersLimitDataset"
/>
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 515dd3de319..1d061a4b81e 100644
--- a/app/assets/javascripts/invite_members/components/user_limit_notification.vue
+++ b/app/assets/javascripts/invite_members/components/user_limit_notification.vue
@@ -1,14 +1,18 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { n__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
import {
+ INFO_ALERT_TITLE,
WARNING_ALERT_TITLE,
DANGER_ALERT_TITLE,
REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE,
REACHED_LIMIT_VARIANT,
CLOSE_TO_LIMIT_MESSAGE,
CLOSE_TO_LIMIT_VARIANT,
+ NOTIFICATION_LIMIT_MESSAGE,
+ NOTIFICATION_LIMIT_VARIANT,
} from '../constants';
export default {
@@ -28,6 +32,15 @@ export default {
computed: {
limitAttributes() {
return {
+ [NOTIFICATION_LIMIT_VARIANT]: {
+ variant: 'info',
+ title: this.notificationTitle(
+ INFO_ALERT_TITLE,
+ this.name,
+ this.usersLimitDataset.freeUsersLimit,
+ ),
+ message: this.message(NOTIFICATION_LIMIT_MESSAGE, this.usersLimitDataset.freeUsersLimit),
+ },
[CLOSE_TO_LIMIT_VARIANT]: {
variant: 'warning',
title: this.title(WARNING_ALERT_TITLE, this.usersLimitDataset.remainingSeats),
@@ -42,6 +55,13 @@ export default {
},
},
methods: {
+ notificationTitle(titleTemplate, namespaceName, dashboardLimit) {
+ return sprintf(titleTemplate, {
+ namespaceName,
+ dashboardLimit,
+ });
+ },
+
title(titleTemplate, count) {
return sprintf(titleTemplate, {
count,
@@ -49,7 +69,14 @@ export default {
name: this.name,
});
},
+
+ message(messageTemplate, dashboardLimit) {
+ return sprintf(messageTemplate, {
+ dashboardLimit,
+ });
+ },
},
+ freeUserLimitHelpPath: helpPagePath('user/free_user_limit'),
};
</script>
@@ -60,6 +87,11 @@ export default {
:title="limitAttributes[limitVariant].title"
>
<gl-sprintf :message="limitAttributes[limitVariant].message">
+ <template #freeUserLimitLink="{ content }">
+ <gl-link :href="$options.freeUserLimitHelpPath" class="gl-label-link">{{
+ content
+ }}</gl-link>
+ </template>
<template #trialLink="{ content }">
<gl-link
:href="usersLimitDataset.newTrialRegistrationPath"
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index a894eb24d38..edc0ebff083 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -139,6 +139,9 @@ export const GROUP_MODAL_LABELS = {
export const LEARN_GITLAB = 'learn_gitlab';
export const ON_SHOW_TRACK_LABEL = 'over_limit_modal_viewed';
+export const INFO_ALERT_TITLE = s__(
+ 'InviteMembersModal|Your top-level group %{namespaceName} is over the %{dashboardLimit} user limit.',
+);
export const WARNING_ALERT_TITLE = s__(
'InviteMembersModal|You only have space for %{count} more %{members} in %{name}',
);
@@ -148,17 +151,22 @@ export const DANGER_ALERT_TITLE = s__(
export const REACHED_LIMIT_VARIANT = 'reached';
export const CLOSE_TO_LIMIT_VARIANT = 'close';
+export const NOTIFICATION_LIMIT_VARIANT = 'notification';
export const REACHED_LIMIT_MESSAGE = s__(
- 'InviteMembersModal|To invite new users to this namespace, you must remove existing users. You can still add existing namespace users.',
+ 'InviteMembersModal|To invite new users to this top-level group, you must remove existing users. You can still add existing users from the top-level group, including any subgroups and projects.',
);
export const REACHED_LIMIT_UPGRADE_SUGGESTION_MESSAGE = REACHED_LIMIT_MESSAGE.concat(
s__(
- 'InviteMembersModal| To get more members, the owner of this namespace can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
+ 'InviteMembersModal| To get more members, the owner of this top-level group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
),
);
export const CLOSE_TO_LIMIT_MESSAGE = s__(
'InviteMembersModal|To get more members an owner of the group can %{trialLinkStart}start a trial%{trialLinkEnd} or %{upgradeLinkStart}upgrade%{upgradeLinkEnd} to a paid tier.',
);
+
+export const NOTIFICATION_LIMIT_MESSAGE = s__(
+ 'InviteMembersModal|GitLab will enforce this limit in the future. If you are over %{dashboardLimit} users when enforcement begins, your top-level group will be placed in a %{freeUserLimitLinkStart}read-only state%{freeUserLimitLinkEnd}. To avoid being placed in a read-only state, reduce your top-level group to %{dashboardLimit} users or less, or purchase a paid tier.',
+);
diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
index 543dca0afe1..14325d6b64e 100644
--- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
+++ b/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
@@ -1,11 +1,16 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import { __ } from '~/locale';
+import { sprintf, __ } from '~/locale';
import { IssuableType, WorkspaceType } from '~/issues/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+const NoteableTypeText = {
+ issue: __('issue'),
+ merge_request: __('merge request'),
+};
+
export default {
WorkspaceType,
IssuableType,
@@ -40,7 +45,9 @@ export default {
iconName: 'spam',
visible: this.hidden,
dataTestId: 'hidden',
- tooltip: __('This issue is hidden because its author has been banned'),
+ tooltip: sprintf(__('This %{issuable} is hidden because its author has been banned'), {
+ issuable: NoteableTypeText[this.getNoteableData.targetType],
+ }),
},
];
},
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index fd55f05e955..c815c7aaba9 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -135,7 +135,6 @@ export default {
<gl-link
:href="computedPath"
class="sortable-link gl-font-weight-normal"
- target="_blank"
@click="handleTitleClick"
>
{{ title }}
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index e8ba99e0e9e..99a3f76ca76 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -47,13 +47,12 @@ function getFallbackKey() {
}
export default class IssuableForm {
- static addAutosave(map, id, $input, searchTerm, fallbackKey) {
- if ($input.length) {
- map.set(
- id,
- new Autosave($input, [document.location.pathname, searchTerm, id], `${fallbackKey}=${id}`),
- );
- }
+ static addAutosave(map, id, element, searchTerm, fallbackKey) {
+ if (!element) return;
+ map.set(
+ id,
+ new Autosave(element, [document.location.pathname, searchTerm, id], `${fallbackKey}=${id}`),
+ );
}
constructor(form) {
@@ -122,28 +121,28 @@ export default class IssuableForm {
IssuableForm.addAutosave(
autosaveMap,
'title',
- this.form.find('input[name*="[title]"]'),
+ this.form.find('input[name*="[title]"]').get(0),
this.searchTerm,
this.fallbackKey,
);
IssuableForm.addAutosave(
autosaveMap,
'description',
- this.form.find('textarea[name*="[description]"]'),
+ this.form.find('textarea[name*="[description]"]').get(0),
this.searchTerm,
this.fallbackKey,
);
IssuableForm.addAutosave(
autosaveMap,
'confidential',
- this.form.find('input:checkbox[name*="[confidential]"]'),
+ this.form.find('input:checkbox[name*="[confidential]"]').get(0),
this.searchTerm,
this.fallbackKey,
);
IssuableForm.addAutosave(
autosaveMap,
'due_date',
- this.form.find('input[name*="[due_date]"]'),
+ this.form.find('input[name*="[due_date]"]').get(0),
this.searchTerm,
this.fallbackKey,
);
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index b9d876ef72f..8edc9a08c9e 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -30,20 +30,35 @@ import { __ } from '~/locale';
import {
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
} 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 { AutocompleteCache } from '../utils';
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
+const EmojiToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
+const LabelToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
+const MilestoneToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
export default {
i18n: {
calendarButtonText: __('Subscribe to calendar'),
closed: __('CLOSED'),
closedMoved: __('CLOSED (MOVED)'),
- emptyStateTitle: __('Please select at least one filter to see results'),
+ emptyStateWithFilterTitle: __('Sorry, your filter produced no results'),
+ emptyStateWithFilterDescription: __('To widen your search, change or remove filters above'),
+ emptyStateWithoutFilterTitle: __('Please select at least one filter to see results'),
errorFetchingIssues: __('An error occurred while loading issues'),
rssButtonText: __('Subscribe to RSS feed'),
searchInputPlaceholder: __('Search or filter results...'),
@@ -60,8 +75,12 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: [
+ 'autocompleteAwardEmojisPath',
'calendarPath',
- 'emptyStateSvgPath',
+ 'dashboardLabelsPath',
+ 'dashboardMilestonesPath',
+ 'emptyStateWithFilterSvgPath',
+ 'emptyStateWithoutFilterSvgPath',
'hasBlockedIssuesFeature',
'hasIssuableHealthStatusFeature',
'hasIssueWeightsFeature',
@@ -117,6 +136,9 @@ export default {
this.issuesError = this.$options.i18n.errorFetchingIssues;
Sentry.captureException(error);
},
+ skip() {
+ return !this.hasSearch;
+ },
debounce: 200,
},
},
@@ -124,6 +146,25 @@ export default {
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
+ emptyStateDescription() {
+ return this.hasSearch ? this.$options.i18n.emptyStateWithFilterDescription : undefined;
+ },
+ emptyStateSvgPath() {
+ return this.hasSearch
+ ? this.emptyStateWithFilterSvgPath
+ : this.emptyStateWithoutFilterSvgPath;
+ },
+ emptyStateTitle() {
+ return this.hasSearch
+ ? this.$options.i18n.emptyStateWithFilterTitle
+ : this.$options.i18n.emptyStateWithoutFilterTitle;
+ },
+ hasSearch() {
+ return Boolean(this.searchQuery || Object.keys(this.urlFilterParams).length);
+ },
+ renderedIssues() {
+ return this.hasSearch ? this.issues : [];
+ },
searchQuery() {
return convertToSearchQuery(this.filterTokens);
},
@@ -159,12 +200,46 @@ export default {
preloadedUsers,
recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-author',
},
+ {
+ type: TOKEN_TYPE_LABEL,
+ title: TOKEN_TITLE_LABEL,
+ icon: 'labels',
+ token: LabelToken,
+ fetchLabels: this.fetchLabels,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-label',
+ },
+ {
+ type: TOKEN_TYPE_MILESTONE,
+ title: TOKEN_TITLE_MILESTONE,
+ icon: 'clock',
+ token: MilestoneToken,
+ fetchMilestones: this.fetchMilestones,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-milestone',
+ shouldSkipSort: true,
+ },
];
+ if (this.isSignedIn) {
+ tokens.push({
+ type: TOKEN_TYPE_MY_REACTION,
+ title: TOKEN_TITLE_MY_REACTION,
+ icon: 'thumb-up',
+ token: EmojiToken,
+ unique: true,
+ fetchEmojis: this.fetchEmojis,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-my_reaction',
+ });
+ }
+
+ tokens.sort((a, b) => a.title.localeCompare(b.title));
+
return tokens;
},
showPaginationControls() {
- return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
+ return (
+ this.renderedIssues.length > 0 &&
+ (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage)
+ );
},
sortOptions() {
return getSortOptions({
@@ -185,7 +260,34 @@ export default {
};
},
},
+ created() {
+ this.autocompleteCache = new AutocompleteCache();
+ },
methods: {
+ fetchEmojis(search) {
+ return this.autocompleteCache.fetch({
+ url: this.autocompleteAwardEmojisPath,
+ cacheName: 'emojis',
+ searchProperty: 'name',
+ search,
+ });
+ },
+ fetchLabels(search) {
+ return this.autocompleteCache.fetch({
+ url: this.dashboardLabelsPath,
+ cacheName: 'labels',
+ searchProperty: 'title',
+ search,
+ });
+ },
+ fetchMilestones(search) {
+ return this.autocompleteCache.fetch({
+ url: this.dashboardMilestonesPath,
+ cacheName: 'milestones',
+ searchProperty: 'title',
+ search,
+ });
+ },
fetchUsers(search) {
return axios.get('/-/autocomplete/users.json', { params: { active: true, search } });
},
@@ -266,7 +368,7 @@ export default {
:has-scoped-labels-feature="hasScopedLabelsFeature"
:initial-filter-value="filterTokens"
:initial-sort-by="sortKey"
- :issuables="issues"
+ :issuables="renderedIssues"
:issuables-loading="$apollo.queries.issues.loading"
namespace="dashboard"
recent-searches-storage-key="issues"
@@ -307,7 +409,11 @@ export default {
</template>
<template #empty-state>
- <gl-empty-state :svg-path="emptyStateSvgPath" :title="$options.i18n.emptyStateTitle" />
+ <gl-empty-state
+ :description="emptyStateDescription"
+ :svg-path="emptyStateSvgPath"
+ :title="emptyStateTitle"
+ />
</template>
</issuable-list>
</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index e3e5cc614cb..005ab5ce3b0 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -14,8 +14,12 @@ export function mountIssuesDashboardApp() {
Vue.use(VueApollo);
const {
+ autocompleteAwardEmojisPath,
calendarPath,
- emptyStateSvgPath,
+ dashboardLabelsPath,
+ dashboardMilestonesPath,
+ emptyStateWithFilterSvgPath,
+ emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
@@ -33,8 +37,12 @@ export function mountIssuesDashboardApp() {
defaultClient: createDefaultClient(),
}),
provide: {
+ autocompleteAwardEmojisPath,
calendarPath,
- emptyStateSvgPath,
+ dashboardLabelsPath,
+ dashboardMilestonesPath,
+ emptyStateWithFilterSvgPath,
+ emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
index 8ffcb456755..43b8804108c 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -7,8 +7,14 @@ query getDashboardIssues(
$search: String
$sort: IssueSort
$state: IssuableState
+ $assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $not: NegatedIssueFilterInput
$afterCursor: String
$beforeCursor: String
$firstPageSize: Int
@@ -18,8 +24,14 @@ query getDashboardIssues(
search: $search
sort: $sort
state: $state
+ assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ not: $not
after: $afterCursor
before: $beforeCursor
first: $firstPageSize
diff --git a/app/assets/javascripts/issues/dashboard/utils.js b/app/assets/javascripts/issues/dashboard/utils.js
new file mode 100644
index 00000000000..6fa95b38649
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/utils.js
@@ -0,0 +1,23 @@
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { MAX_LIST_SIZE } from '~/issues/list/constants';
+import axios from '~/lib/utils/axios_utils';
+
+export class AutocompleteCache {
+ constructor() {
+ this.cache = {};
+ }
+
+ fetch({ url, cacheName, searchProperty, search }) {
+ if (this.cache[cacheName]) {
+ const data = search
+ ? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchProperty })
+ : this.cache[cacheName].slice(0, MAX_LIST_SIZE);
+ return Promise.resolve(data);
+ }
+
+ return axios.get(url).then(({ data }) => {
+ this.cache[cacheName] = data;
+ return data.slice(0, MAX_LIST_SIZE);
+ });
+ }
+}
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 12a83f06453..e4000184f41 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -352,6 +352,7 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
+ operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
fetchLabels: this.fetchLabels,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 49a953cad43..87184799d5f 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -159,7 +159,7 @@ export const TYPE_TOKEN_OBJECTIVE_OPTION = {
};
export const TYPE_TOKEN_KEY_RESULT_OPTION = {
- icon: 'issue-type-key-result',
+ icon: 'issue-type-keyresult',
title: 'key_result',
value: 'key_result',
};
@@ -247,6 +247,7 @@ export const filters = {
[API_PARAM]: {
[NORMAL_FILTER]: 'labelName',
[SPECIAL_FILTER]: 'labelName',
+ [ALTERNATIVE_FILTER]: 'labelNames',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -257,6 +258,9 @@ export const filters = {
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
+ [OPERATOR_OR]: {
+ [ALTERNATIVE_FILTER]: 'or[label_name][]',
+ },
},
},
[TOKEN_TYPE_TYPE]: {
@@ -360,14 +364,17 @@ export const filters = {
},
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
- [NORMAL_FILTER]: 'healthStatus',
- [SPECIAL_FILTER]: 'healthStatus',
+ [NORMAL_FILTER]: 'healthStatusFilter',
+ [SPECIAL_FILTER]: 'healthStatusFilter',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
[SPECIAL_FILTER]: 'health_status',
},
+ [OPERATOR_NOT]: {
+ [NORMAL_FILTER]: 'not[health_status]',
+ },
},
},
[TOKEN_TYPE_CONTACT]: {
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index b566e08731c..bbd081843ca 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -13,6 +13,8 @@ import {
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_LABEL,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
ALTERNATIVE_FILTER,
@@ -252,8 +254,9 @@ const isSpecialFilter = (type, data) => {
const getFilterType = ({ type, value: { data, operator } }) => {
const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR;
+ const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR;
- if (isUnionedAuthor) {
+ if (isUnionedAuthor || isUnionedLabel) {
return ALTERNATIVE_FILTER;
}
if (isSpecialFilter(type, data)) {
@@ -267,8 +270,13 @@ const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_R
const isWildcardValue = (tokenType, value) =>
wildcardTokens.includes(tokenType) && specialFilterValues.includes(value);
+const isHealthStatusSpecialFilter = (tokenType, value) =>
+ tokenType === TOKEN_TYPE_HEALTH && specialFilterValues.includes(value);
+
const requiresUpperCaseValue = (tokenType, value) =>
- tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
+ tokenType === TOKEN_TYPE_TYPE ||
+ isWildcardValue(tokenType, value) ||
+ isHealthStatusSpecialFilter(tokenType, value);
const formatData = (token) => {
if (requiresUpperCaseValue(token.type, token.value.data)) {
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 983e2e6530e..56e360c75e3 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -19,6 +19,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
import DeleteIssueModal from './delete_issue_modal.vue';
@@ -50,6 +51,7 @@ export default {
GlDropdownItem,
GlLink,
GlModal,
+ AbuseCategorySelector,
},
directives: {
GlModal: GlModalDirective,
@@ -93,13 +95,15 @@ export default {
projectPath: {
default: '',
},
- reportAbusePath: {
- default: '',
- },
submitAsSpamPath: {
default: '',
},
},
+ data() {
+ return {
+ isReportAbuseDrawerOpen: false,
+ };
+ },
computed: {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
@@ -163,6 +167,9 @@ export default {
this.invokeUpdateIssueMutation();
},
+ toggleReportAbuseDrawer(isOpen) {
+ this.isReportAbuseDrawerOpen = isOpen;
+ },
invokeUpdateIssueMutation() {
this.toggleStateButtonLoading(true);
@@ -255,7 +262,7 @@ export default {
<gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
+ <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
{{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
@@ -314,7 +321,7 @@ export default {
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
+ <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
{{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
@@ -360,5 +367,10 @@ export default {
:modal-id="$options.deleteModalId"
:title="deleteButtonText"
/>
+
+ <abuse-category-selector
+ :show-drawer="isReportAbuseDrawerOpen"
+ @close-drawer="toggleReportAbuseDrawer(false)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index 22db19610c1..2fdae538902 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({
'Incident|Something went wrong while creating the incident timeline event.',
),
areaPlaceholder: s__('Incident|Timeline text...'),
+ areaDefaultMessage: s__('Incident|Incident'),
+ selectTags: __('Select tags'),
+ tagsLabel: __('Event tag (optional)'),
save: __('Save'),
cancel: __('Cancel'),
delete: __('Delete'),
@@ -42,4 +45,14 @@ export const timelineItemI18n = Object.freeze({
timeUTC: __('%{time} UTC'),
});
+export const timelineEventTagsI18n = Object.freeze({
+ startTime: __('Start time'),
+ endTime: __('End time'),
+});
+
export const MAX_TEXT_LENGTH = 280;
+
+export const TIMELINE_EVENT_TAGS = Object.values(timelineEventTagsI18n).map((item) => ({
+ text: item,
+ value: item,
+}));
diff --git a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
index 6bb72e82778..81111d42b39 100644
--- a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
@@ -74,6 +74,7 @@ export default {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
note: eventDetails.note,
occurredAt: eventDetails.occurredAt,
+ timelineEventTagNames: eventDetails.timelineEventTags,
},
},
update: this.updateCache,
diff --git a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
index 8cdd62ca9ef..4ef9b9c5a99 100644
--- a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -40,7 +40,7 @@ export default {
:is-event-processed="editTimelineEventActive"
:previous-occurred-at="event.occurredAt"
:previous-note="event.note"
- show-delete
+ is-editing
@save-event="saveEvent"
@cancel="$emit('hide-edit')"
@delete="$emit('delete')"
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 f1a3aebc990..6648e20865d 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
@@ -1,7 +1,9 @@
<script>
-import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
+import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlListbox } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __, sprintf } from '~/locale';
+import { MAX_TEXT_LENGTH, TIMELINE_EVENT_TAGS, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
export default {
@@ -23,7 +25,9 @@ export default {
GlFormInput,
GlFormGroup,
GlButton,
+ GlListbox,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: timelineFormI18n,
MAX_TEXT_LENGTH,
props: {
@@ -32,7 +36,7 @@ export default {
required: false,
default: false,
},
- showDelete: {
+ isEditing: {
type: Boolean,
required: false,
default: false,
@@ -51,6 +55,16 @@ export default {
required: false,
default: '',
},
+ previousTags: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ tags: {
+ type: Array,
+ required: false,
+ default: () => TIMELINE_EVENT_TAGS,
+ },
},
data() {
// if occurredAt is null, returns "now" in UTC
@@ -58,10 +72,12 @@ export default {
return {
timelineText: this.previousNote,
+ timelineTextIsDirty: this.isEditing,
placeholderDate,
hourPickerInput: placeholderDate.getHours(),
minutePickerInput: placeholderDate.getMinutes(),
datePickerInput: placeholderDate,
+ selectedTags: [...this.previousTags],
};
},
computed: {
@@ -85,6 +101,20 @@ export default {
timelineTextCount() {
return this.timelineText.length;
},
+ dropdownText() {
+ if (!this.selectedTags.length) {
+ return timelineFormI18n.selectTags;
+ }
+
+ const dropdownText =
+ this.selectedTags.length === 1
+ ? this.selectedTags[0]
+ : sprintf(__('%{numberOfSelectedTags} tags'), {
+ numberOfSelectedTags: this.selectedTags.length,
+ });
+
+ return dropdownText;
+ },
},
mounted() {
this.focusDate();
@@ -96,14 +126,35 @@ export default {
this.hourPickerInput = newPlaceholderDate.getHours();
this.minutePickerInput = newPlaceholderDate.getMinutes();
this.timelineText = '';
+ this.selectedTags = [];
},
focusDate() {
this.$refs.datepicker.$el.querySelector('input')?.focus();
},
+ setTimelineTextDirty() {
+ this.timelineTextIsDirty = true;
+ },
+ onTagsChange(tagValue) {
+ this.selectedTags = [...tagValue];
+
+ if (!this.timelineTextIsDirty) {
+ this.timelineText = this.generateTimelineTextFromTags(this.selectedTags);
+ }
+ },
+ generateTimelineTextFromTags(tags) {
+ if (!tags.length) {
+ return '';
+ }
+
+ const tagsMessage = tags.map((tag) => tag.toLocaleLowerCase()).join(', ');
+
+ return `${timelineFormI18n.areaDefaultMessage} ${tagsMessage}`;
+ },
handleSave(addAnotherEvent) {
const event = {
note: this.timelineText,
occurredAt: this.occurredAtString,
+ timelineEventTags: this.selectedTags,
};
this.$emit('save-event', event, addAnotherEvent);
},
@@ -146,6 +197,16 @@ export default {
<p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p>
</div>
</div>
+ <gl-form-group v-if="glFeatures.incidentEventTags" :label="$options.i18n.tagsLabel">
+ <gl-listbox
+ :selected="selectedTags"
+ :toggle-text="dropdownText"
+ :items="tags"
+ :is-check-centered="true"
+ :multiple="true"
+ @select="onTagsChange"
+ />
+ </gl-form-group>
<div class="common-note-form">
<gl-form-group class="gl-mb-3" :label="$options.i18n.areaLabel">
<markdown-field
@@ -169,6 +230,7 @@ export default {
aria-describedby="timeline-form-hint"
:placeholder="$options.i18n.areaPlaceholder"
:maxlength="$options.MAX_TEXT_LENGTH"
+ @input="setTimelineTextDirty"
>
</textarea>
<div id="timeline-form-hint" class="gl-sr-only">{{ $options.i18n.hint }}</div>
@@ -214,7 +276,7 @@ export default {
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
- v-if="showDelete"
+ v-if="isEditing"
class="gl-ml-auto btn-danger"
:disabled="isEventProcessed"
@click="$emit('delete')"
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 3cb5007ab0d..21d877c5fe6 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -83,7 +83,7 @@ export function initIssueApp(issueData, store) {
return undefined;
}
- const { fullPath } = el.dataset;
+ const { fullPath, registerPath, signInPath } = el.dataset;
scrollToTargetOnResize();
@@ -99,6 +99,8 @@ export function initIssueApp(issueData, store) {
provide: {
canCreateIncident,
fullPath,
+ registerPath,
+ signInPath,
hasIssueWeightsFeature,
},
computed: {
@@ -150,6 +152,8 @@ export function initHeaderActions(store, type = '') {
projectPath: el.dataset.projectPath,
projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath,
+ reportedUserId: el.dataset.reportedUserId,
+ reportedFromUrl: el.dataset.reportedFromUrl,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},
render: (createElement) => createElement(HeaderActions),
diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
index 9b36642feb7..dd9afb01590 100644
--- a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
@@ -1,11 +1,6 @@
<script>
-import {
- GlDropdown,
- GlSearchBoxByType,
- GlLoadingIcon,
- GlDropdownItem,
- GlAvatarLabeled,
-} from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __ } from '~/locale';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
import { PROJECTS_PER_PAGE } from '../constants';
@@ -17,11 +12,8 @@ export default {
endCursor: '',
},
components: {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlLoadingIcon,
GlAvatarLabeled,
+ GlCollapsibleListbox,
},
props: {
selectedProject: {
@@ -34,6 +26,7 @@ export default {
return {
initialProjectsLoading: true,
projectSearchQuery: '',
+ selectedProjectId: this.selectedProject?.id,
};
},
apollo: {
@@ -66,17 +59,27 @@ export default {
projectDropdownText() {
return this.selectedProject?.nameWithNamespace || this.$options.i18n.selectProjectText;
},
+ projectList() {
+ return (this.projects || []).map((project) => ({
+ ...project,
+ text: project.nameWithNamespace,
+ value: String(project.id),
+ }));
+ },
},
methods: {
- onProjectSelect(project) {
- this.$emit('change', project);
+ findProjectById(id) {
+ return this.projects.find((project) => id === project.id);
+ },
+ onProjectSelect(projectId) {
+ this.$emit('change', this.findProjectById(projectId));
},
onError({ message } = {}) {
this.$emit('error', { message });
},
- isProjectSelected(project) {
- return project.id === this.selectedProject?.id;
- },
+ onSearch: debounce(function debouncedSearch(query) {
+ this.projectSearchQuery = query;
+ }, 250),
},
i18n: {
selectProjectText: __('Select a project'),
@@ -86,37 +89,29 @@ export default {
</script>
<template>
- <gl-dropdown
- :text="projectDropdownText"
- :loading="initialProjectsLoading"
- menu-class="gl-w-auto!"
+ <gl-collapsible-listbox
+ v-model="selectedProjectId"
+ data-testid="project-select"
+ :items="projectList"
+ :toggle-text="projectDropdownText"
:header-text="$options.i18n.selectProjectText"
+ :loading="initialProjectsLoading"
+ :searchable="true"
+ :searching="projectsLoading"
+ @search="onSearch"
+ @select="onProjectSelect"
>
- <template #header>
- <gl-search-box-by-type v-model.trim="projectSearchQuery" :debounce="250" />
- </template>
-
- <gl-loading-icon v-show="projectsLoading" />
- <template v-if="!projectsLoading">
- <gl-dropdown-item
- v-for="project in projects"
- :key="project.id"
- is-check-item
- is-check-centered
- :is-checked="isProjectSelected(project)"
- :data-testid="`test-project-${project.id}`"
- @click="onProjectSelect(project)"
- >
- <gl-avatar-labeled
- class="gl-text-truncate"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- :size="32"
- :src="project.avatarUrl"
- :label="project.name"
- :entity-name="project.name"
- :sub-label="project.nameWithNamespace"
- />
- </gl-dropdown-item>
+ <template #list-item="{ item: project }">
+ <gl-avatar-labeled
+ v-if="project"
+ class="gl-text-truncate"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="32"
+ :src="project.avatarUrl"
+ :label="project.name"
+ :entity-name="project.name"
+ :sub-label="project.nameWithNamespace"
+ />
</template>
- </gl-dropdown>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
index 22a6c0751f4..44575455a34 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue
@@ -10,7 +10,6 @@ import { SET_ALERT } from '../store/mutation_types';
import SignInPage from '../pages/sign_in/sign_in_page.vue';
import SubscriptionsPage from '../pages/subscriptions_page.vue';
import UserLink from './user_link.vue';
-import CompatibilityAlert from './compatibility_alert.vue';
import BrowserSupportAlert from './browser_support_alert.vue';
export default {
@@ -19,11 +18,10 @@ export default {
GlAlert,
GlLink,
GlSprintf,
- UserLink,
- CompatibilityAlert,
BrowserSupportAlert,
SignInPage,
SubscriptionsPage,
+ UserLink,
},
mixins: [glFeatureFlagMixin()],
inject: {
@@ -123,8 +121,6 @@ export default {
<main class="jira-connect-app gl-px-5 gl-pt-7 gl-mx-auto">
<browser-support-alert v-if="!isBrowserSupported" class="gl-mb-7" />
<div v-else data-testid="jira-connect-app">
- <compatibility-alert class="gl-mb-7" />
-
<gl-alert
v-if="shouldShowAlert"
:variant="alert.variant"
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue b/app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue
deleted file mode 100644
index 9b50681515e..00000000000
--- a/app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-const COMPATIBILITY_ALERT_STATE_KEY = 'compatibility_alert_dismissed';
-
-export default {
- name: 'CompatibilityAlert',
- components: {
- GlAlert,
- GlSprintf,
- GlLink,
- LocalStorageSync,
- },
- mixins: [glFeatureFlagMixin()],
- data() {
- return {
- alertDismissed: false,
- };
- },
- computed: {
- shouldShowAlert() {
- return !this.alertDismissed;
- },
- isOauthSelfManagedEnabled() {
- return this.glFeatures.jiraConnectOauth && this.glFeatures.jiraConnectOauthSelfManaged;
- },
- alertBody() {
- return this.isOauthSelfManagedEnabled
- ? this.$options.i18n.body
- : this.$options.i18n.bodyDotCom;
- },
- },
- methods: {
- dismissAlert() {
- this.alertDismissed = true;
- },
- },
- i18n: {
- title: s__('Integrations|Known limitations'),
- body: s__(
- 'Integrations|Adding a namespace only works in browsers that allow cross-site cookies. %{linkStart}Learn more%{linkEnd}.',
- ),
- bodyDotCom: s__(
- 'Integrations|This integration only works with GitLab.com. Adding a namespace only works in browsers that allow cross-site cookies. %{linkStart}Learn more%{linkEnd}.',
- ),
- },
- DOCS_LINK_URL: helpPagePath('integration/jira/connect-app'),
- COMPATIBILITY_ALERT_STATE_KEY,
-};
-</script>
-<template>
- <local-storage-sync
- v-model="alertDismissed"
- :storage-key="$options.COMPATIBILITY_ALERT_STATE_KEY"
- >
- <gl-alert
- v-if="shouldShowAlert"
- variant="info"
- :title="$options.i18n.title"
- @dismiss="dismissAlert"
- >
- <gl-sprintf :message="alertBody">
- <template #link="{ content }">
- <gl-link :href="$options.DOCS_LINK_URL" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- </local-storage-sync>
-</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
index 4cf3a1a0279..65c69bcfa82 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_oauth_button.vue
@@ -4,6 +4,7 @@ import { GlButton } from '@gitlab/ui';
import { sprintf } from '~/locale';
import {
+ GITLAB_COM_BASE_PATH,
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
I18N_CUSTOM_SIGN_IN_BUTTON_TEXT,
I18N_OAUTH_APPLICATION_ID_ERROR_MESSAGE,
@@ -40,8 +41,11 @@ export default {
};
},
computed: {
+ isGitlabCom() {
+ return this.gitlabBasePath === GITLAB_COM_BASE_PATH;
+ },
buttonText() {
- if (!this.gitlabBasePath) {
+ if (this.isGitlabCom) {
return I18N_DEFAULT_SIGN_IN_BUTTON_TEXT;
}
@@ -71,9 +75,9 @@ export default {
this.codeVerifier = createCodeVerifier();
const codeChallenge = await createCodeChallenge(this.codeVerifier);
try {
- this.clientId = this.gitlabBasePath
- ? await this.fetchOauthClientId()
- : this.oauthMetadata?.oauth_token_payload?.client_id;
+ this.clientId = this.isGitlabCom
+ ? this.oauthMetadata?.oauth_token_payload?.client_id
+ : await this.fetchOauthClientId();
} catch {
throw new Error(I18N_OAUTH_APPLICATION_ID_ERROR_MESSAGE);
}
@@ -92,7 +96,7 @@ export default {
);
// Rebase URL on the specified GitLab base path (if specified).
- if (this.gitlabBasePath) {
+ if (!this.isGitlabCom) {
const gitlabBasePathURL = new URL(this.gitlabBasePath);
oauthAuthorizeURLWithChallenge.hostname = gitlabBasePathURL.hostname;
oauthAuthorizeURLWithChallenge.pathname = `${
@@ -118,7 +122,7 @@ export default {
this.setAlert({
linkUrl: OAUTH_SELF_MANAGED_DOC_LINK,
title: I18N_OAUTH_FAILED_TITLE,
- message: this.gitlabBasePath ? I18N_OAUTH_FAILED_MESSAGE : '',
+ message: this.isGitlabCom ? '' : I18N_OAUTH_FAILED_MESSAGE,
variant: 'danger',
});
}
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue
index 91b66c87694..782e8a625a9 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_com.vue
@@ -1,6 +1,7 @@
<script>
import { s__ } from '~/locale';
+import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SubscriptionsList from '../../components/subscriptions_list.vue';
@@ -28,6 +29,7 @@ export default {
signInButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'),
signInText: s__('JiraService|Sign in to GitLab.com to get started.'),
},
+ GITLAB_COM_BASE_PATH,
methods: {
onSignInError() {
this.$emit('error');
@@ -43,6 +45,7 @@ export default {
<div class="gl-display-flex gl-justify-content-end gl-mb-3">
<sign-in-oauth-button
v-if="useSignInOauthButton"
+ :gitlab-base-path="$options.GITLAB_COM_BASE_PATH"
@sign-in="$emit('sign-in-oauth', $event)"
@error="onSignInError"
>
@@ -59,6 +62,7 @@ export default {
<p class="gl-mb-7">{{ $options.i18n.signInText }}</p>
<sign-in-oauth-button
v-if="useSignInOauthButton"
+ :gitlab-base-path="$options.GITLAB_COM_BASE_PATH"
@sign-in="$emit('sign-in-oauth', $event)"
@error="onSignInError"
/>
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
index d7bbd6daed2..734d3ca0d49 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
@@ -68,6 +68,7 @@ export default {
required: true,
},
},
+ clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0'],
inputTypes: {
key: 'key',
value: 'value',
@@ -229,16 +230,23 @@ export default {
v-gl-tooltip
:aria-label="$options.i18n.clearInputs"
:title="$options.i18n.clearInputs"
- class="gl-flex-grow-0 gl-flex-basis-0"
+ :class="$options.clearBtnSharedClasses"
category="tertiary"
variant="danger"
icon="clear"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
-
- <!-- delete variable button placeholder to not break flex layout -->
- <div v-else class="gl-w-7 gl-mr-3" data-testid="delete-variable-btn-placeholder"></div>
+ <!-- Placeholder button to keep the layout fixed -->
+ <gl-button
+ v-else
+ class="gl-opacity-0 gl-pointer-events-none"
+ :class="$options.clearBtnSharedClasses"
+ data-testid="delete-variable-btn-placeholder"
+ category="tertiary"
+ variant="danger"
+ icon="clear"
+ />
</div>
<div class="gl-text-center gl-mt-5">
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index d8c5c292f52..9ee4439b618 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -1,7 +1,7 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import ActionsCell from './cells/actions_cell.vue';
import DurationCell from './cells/duration_cell.vue';
import JobCell from './cells/job_cell.vue';
@@ -14,7 +14,7 @@ export default {
},
components: {
ActionsCell,
- CiBadge,
+ CiBadgeLink,
DurationCell,
GlTable,
JobCell,
@@ -55,7 +55,7 @@ export default {
</template>
<template #cell(status)="{ item }">
- <ci-badge :status="item.detailedStatus" />
+ <ci-badge-link :status="item.detailedStatus" />
</template>
<template #cell(job)="{ item }">
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 272181f830c..a81edb240ad 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -2,7 +2,7 @@ import Visibility from 'visibilityjs';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_FORBIDDEN } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import {
canScroll,
@@ -178,7 +178,7 @@ export const fetchJobLog = ({ dispatch, state }) =>
}
})
.catch((e) => {
- if (e.response.status === httpStatusCodes.FORBIDDEN) {
+ if (e.response.status === HTTP_STATUS_FORBIDDEN) {
dispatch('receiveJobLogUnauthorizedError');
} else {
reportToSentry('job_actions', e);
diff --git a/app/assets/javascripts/language_switcher/components/app.vue b/app/assets/javascripts/language_switcher/components/app.vue
index 71babe6c614..4d3fe22e247 100644
--- a/app/assets/javascripts/language_switcher/components/app.vue
+++ b/app/assets/javascripts/language_switcher/components/app.vue
@@ -1,11 +1,17 @@
<script>
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
import { setCookie } from '~/lib/utils/common_utils';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { PREFERRED_LANGUAGE_COOKIE_KEY } from '../constants';
+const HELP_TRANSLATE_MSG = __('Help translate to your language');
+const HELP_TRANSLATE_HREF = helpPagePath('/development/i18n/translation.md');
+
export default {
components: {
GlCollapsibleListbox,
+ GlLink,
},
inject: {
locales: {
@@ -25,7 +31,12 @@ export default {
setCookie(PREFERRED_LANGUAGE_COOKIE_KEY, code);
window.location.reload();
},
+ itemTestSelector(locale) {
+ return `language_switcher_lang_${locale}`;
+ },
},
+ HELP_TRANSLATE_MSG,
+ HELP_TRANSLATE_HREF,
};
</script>
<template>
@@ -41,9 +52,20 @@ export default {
@select="onLanguageSelected"
>
<template #list-item="{ item: locale }">
- <span :data-testid="`language_switcher_lang_${locale.value}`">
+ <span
+ :data-testid="itemTestSelector(locale.value)"
+ :data-qa-selector="itemTestSelector(locale.value)"
+ >
{{ locale.text }}
</span>
</template>
+ <template #footer>
+ <div
+ class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-100 gl-display-flex gl-justify-content-center gl-p-3"
+ data-testid="footer"
+ >
+ <gl-link :href="$options.HELP_TRANSLATE_HREF">{{ $options.HELP_TRANSLATE_MSG }}</gl-link>
+ </div>
+ </template>
</gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index ab83f1ecc14..90c1b31286a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -12,8 +12,45 @@ function hideEndFade($scrollingTabs) {
});
}
+export function initScrollingTabs() {
+ const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
+ $scrollingTabs.addClass('is-initialized');
+
+ $(window)
+ .on('resize.nav', () => {
+ hideEndFade($scrollingTabs);
+ })
+ .trigger('resize.nav');
+
+ $scrollingTabs.on('scroll', function tabsScrollEvent() {
+ const $this = $(this);
+ const currentPosition = $this.scrollLeft();
+ const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+
+ $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
+ $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
+ });
+
+ $scrollingTabs.each(function scrollTabsEachLoop() {
+ const $this = $(this);
+ const scrollingTabWidth = $this.width();
+ const $active = $this.find('.active');
+ const activeWidth = $active.width();
+
+ if ($active.length) {
+ const offset = $active.offset().left + activeWidth;
+
+ if (offset > scrollingTabWidth - 30) {
+ const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2;
+
+ $this.scrollLeft(scrollLeft);
+ }
+ }
+ });
+}
+
function initDeferred() {
- $(document).trigger('init.scrolling-tabs');
+ initScrollingTabs();
const appEl = document.getElementById('whats-new-app');
if (!appEl) return;
@@ -34,43 +71,5 @@ export default function initLayoutNav() {
initFlyOutNav();
- // We need to init it on DomContentLoaded as others could also call it
- $(document).on('init.scrolling-tabs', () => {
- const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
- $scrollingTabs.addClass('is-initialized');
-
- $(window)
- .on('resize.nav', () => {
- hideEndFade($scrollingTabs);
- })
- .trigger('resize.nav');
-
- $scrollingTabs.on('scroll', function tabsScrollEvent() {
- const $this = $(this);
- const currentPosition = $this.scrollLeft();
- const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
-
- $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
- $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
- });
-
- $scrollingTabs.each(function scrollTabsEachLoop() {
- const $this = $(this);
- const scrollingTabWidth = $this.width();
- const $active = $this.find('.active');
- const activeWidth = $active.width();
-
- if ($active.length) {
- const offset = $active.offset().left + activeWidth;
-
- if (offset > scrollingTabWidth - 30) {
- const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2;
-
- $this.scrollLeft(scrollLeft);
- }
- }
- });
- });
-
requestIdleCallback(initDeferred);
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 4ce63d518a6..241488c8039 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -142,7 +142,7 @@ export const getOuterHeight = (selector) => {
const element = document.querySelector(selector);
if (!element) {
- return undefined;
+ return 0;
}
return element.offsetHeight;
@@ -154,6 +154,11 @@ export const contentTop = () => {
() => getOuterHeight('#js-peek'),
() => getOuterHeight('.navbar-gitlab'),
({ desktop }) => {
+ const mrStickyHeader = document.querySelector('.merge-request-sticky-header');
+ if (mrStickyHeader) {
+ return mrStickyHeader.offsetHeight;
+ }
+
const container = document.querySelector('.discussions-counter');
let size = 0;
@@ -161,11 +166,12 @@ export const contentTop = () => {
size = container.offsetHeight;
}
+ size += getOuterHeight('.merge-request-tabs');
+ size += getOuterHeight('.issue-sticky-header.gl-fixed');
+
return size;
},
- () => getOuterHeight('.merge-request-sticky-header, .merge-request-tabs'),
() => getOuterHeight('.js-diff-files-changed'),
- () => getOuterHeight('.issue-sticky-header.gl-fixed'),
({ desktop }) => {
const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
let size;
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 4e0a59d0a38..9eb812b8694 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -531,7 +531,7 @@ export const getOverlappingDaysInPeriods = (givenPeriodLeft = {}, givenPeriodRig
/**
* Mimics the behaviour of the rails distance_of_time_in_words function
- * https://api.rubyonrails.org/v6.0.1/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
+ * https://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html#method-i-distance_of_time_in_words
* 0 < -> 29 secs => less than a minute
* 30 secs < -> 1 min, 29 secs => 1 minute
* 1 min, 30 secs < -> 44 mins, 29 secs => [2..44] minutes
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index 737c18d1bce..04a82836f69 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -414,21 +414,21 @@ export const durationTimeFormatted = (duration) => {
*
* @param {Number} offset UTC offset in seconds as a integer
*
- * @return {String} the + or - offset in hours, e.g. `- 10`, `0`, `+ 4`
+ * @return {String} the + or - offset in hours, e.g. `-10`, ` 0`, `+4`
*/
export const formatUtcOffset = (offset) => {
const parsed = parseInt(offset, 10);
if (Number.isNaN(parsed) || parsed === 0) {
- return `0`;
+ return ` 0`;
}
const prefix = offset > 0 ? '+' : '-';
- return `${prefix} ${Math.abs(offset / 3600)}`;
+ return `${prefix}${Math.abs(offset / 3600)}`;
};
/**
* Returns formatted timezone
*
* @param {Object} timezone item with offset and name
- * @returns {String} the UTC timezone with the offset, e.g. `[UTC + 2] Berlin`
+ * @returns {String} the UTC timezone with the offset, e.g. `[UTC+2] Berlin, [UTC 0] London`
*/
-export const formatTimezone = ({ offset, name }) => `[UTC ${formatUtcOffset(offset)}] ${name}`;
+export const formatTimezone = ({ offset, name }) => `[UTC${formatUtcOffset(offset)}] ${name}`;
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index ec0d8d433a5..678ebc35565 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -1,4 +1,5 @@
export const HTTP_STATUS_ABORTED = 0;
+export const HTTP_STATUS_OK = 200;
export const HTTP_STATUS_CREATED = 201;
export const HTTP_STATUS_ACCEPTED = 202;
export const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
@@ -14,21 +15,15 @@ export const HTTP_STATUS_GONE = 410;
export const HTTP_STATUS_PAYLOAD_TOO_LARGE = 413;
export const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
export const HTTP_STATUS_TOO_MANY_REQUESTS = 429;
-
-// TODO move the rest of the status codes to primitive constants
-// https://docs.gitlab.com/ee/development/fe_guide/style/javascript.html#export-constants-as-primitives
-const httpStatusCodes = {
- OK: 200,
- BAD_REQUEST: 400,
- UNAUTHORIZED: 401,
- FORBIDDEN: 403,
- NOT_FOUND: 404,
- INTERNAL_SERVER_ERROR: 500,
- SERVICE_UNAVAILABLE: 503,
-};
+export const HTTP_STATUS_BAD_REQUEST = 400;
+export const HTTP_STATUS_UNAUTHORIZED = 401;
+export const HTTP_STATUS_FORBIDDEN = 403;
+export const HTTP_STATUS_NOT_FOUND = 404;
+export const HTTP_STATUS_INTERNAL_SERVER_ERROR = 500;
+export const HTTP_STATUS_SERVICE_UNAVAILABLE = 503;
export const successCodes = [
- httpStatusCodes.OK,
+ HTTP_STATUS_OK,
HTTP_STATUS_CREATED,
HTTP_STATUS_ACCEPTED,
HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION,
@@ -39,5 +34,3 @@ export const successCodes = [
HTTP_STATUS_ALREADY_REPORTED,
HTTP_STATUS_IM_USED,
];
-
-export default httpStatusCodes;
diff --git a/app/assets/javascripts/lib/utils/poll_until_complete.js b/app/assets/javascripts/lib/utils/poll_until_complete.js
index 3545db3a227..dbe54dceb52 100644
--- a/app/assets/javascripts/lib/utils/poll_until_complete.js
+++ b/app/assets/javascripts/lib/utils/poll_until_complete.js
@@ -1,5 +1,5 @@
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from './http_status';
+import { HTTP_STATUS_OK } from './http_status';
import Poll from './poll';
/**
@@ -30,7 +30,7 @@ export default (url, config = {}) =>
data: { url, config },
method: 'axiosGet',
successCallback: (response) => {
- if (response.status === httpStatusCodes.OK) {
+ if (response.status === HTTP_STATUS_OK) {
resolve(response);
eTagPoll.stop();
}
diff --git a/app/assets/javascripts/locale/ensure_single_line.js b/app/assets/javascripts/locale/ensure_single_line.cjs
index c2c63777001..c2c63777001 100644
--- a/app/assets/javascripts/locale/ensure_single_line.js
+++ b/app/assets/javascripts/locale/ensure_single_line.cjs
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index ad01da2eb17..c1afabf1e35 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,5 +1,5 @@
import Jed from 'jed';
-import ensureSingleLine from './ensure_single_line';
+import ensureSingleLine from './ensure_single_line.cjs';
import sprintf from './sprintf';
const GITLAB_FALLBACK_LANGUAGE = 'en';
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index df3b55ed2ad..fd5c4abe729 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -30,6 +30,7 @@ import initLogoAnimation from './logo';
import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
+import { initSidebarTracking } from './pages/shared/nav/sidebar_tracking';
import initServicePingConsent from './service_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
@@ -99,10 +100,19 @@ function deferredInitialisation() {
initBroadcastNotifications();
initPersistentUserCallouts();
initDefaultTrackers();
+ initSidebarTracking();
initFeatureHighlight();
initCopyCodeButton();
initGitlabVersionCheck();
+ // Init super sidebar
+ if (gon.use_new_navigation) {
+ // eslint-disable-next-line promise/catch-or-return
+ import('./super_sidebar/super_sidebar_bundle').then(({ initSuperSidebar }) => {
+ initSuperSidebar();
+ });
+ }
+
addSelectOnFocusBehaviour('.js-select-on-focus');
const glTooltipDelay = localStorage.getItem('gl-tooltip-delay');
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 f4893721b9e..164fed308ff 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
@@ -49,8 +49,6 @@ export default {
:message="message"
:title="s__('Member|Deny access')"
:is-access-request="true"
- icon="close"
- button-category="primary"
/>
</div>
</action-button-group>
diff --git a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
index 112f722c632..90034f46e7c 100644
--- a/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/approve_access_request_button.vue
@@ -40,7 +40,6 @@ export default {
:title="$options.title"
:aria-label="$options.title"
icon="check"
- variant="confirm"
type="submit"
/>
</gl-form>
diff --git a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
index ab9abfd38c6..91062c222f4 100644
--- a/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/invite_action_buttons.vue
@@ -41,8 +41,6 @@ export default {
<remove-member-button
:member-id="member.id"
:message="message"
- icon="remove"
- button-category="primary"
:title="s__('Member|Revoke invite')"
is-invite
/>
diff --git a/app/assets/javascripts/members/components/action_buttons/leave_button.vue b/app/assets/javascripts/members/components/action_buttons/leave_button.vue
deleted file mode 100644
index f600a207b8d..00000000000
--- a/app/assets/javascripts/members/components/action_buttons/leave_button.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<script>
-import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { LEAVE_MODAL_ID } from '../../constants';
-import LeaveModal from '../modals/leave_modal.vue';
-
-export default {
- name: 'LeaveButton',
- title: __('Leave'),
- modalId: LEAVE_MODAL_ID,
- components: {
- GlButton,
- LeaveModal,
- },
- directives: {
- GlModal: GlModalDirective,
- GlTooltip: GlTooltipDirective,
- },
- props: {
- member: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-button
- v-gl-tooltip.hover
- v-gl-modal="$options.modalId"
- :title="$options.title"
- :aria-label="$options.title"
- icon="leave"
- variant="danger"
- />
- <leave-modal :member="member" />
- </div>
-</template>
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index fef7940eaa2..24500fbe44d 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
@@ -32,7 +32,6 @@ export default {
<template>
<gl-button
v-gl-tooltip.hover
- variant="danger"
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
index 27c67e84675..4b3bb89da55 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_member_button.vue
@@ -14,34 +14,13 @@ export default {
type: Number,
required: true,
},
- memberType: {
- type: String,
- required: false,
- default: null,
- },
message: {
type: String,
required: true,
},
title: {
type: String,
- required: false,
- default: null,
- },
- icon: {
- type: String,
- required: false,
- default: undefined,
- },
- buttonText: {
- type: String,
- required: false,
- default: '',
- },
- buttonCategory: {
- type: String,
- required: false,
- default: 'secondary',
+ required: true,
},
isAccessRequest: {
type: Boolean,
@@ -70,7 +49,6 @@ export default {
isAccessRequest: this.isAccessRequest,
isInvite: this.isInvite,
memberPath: this.memberPath.replace(':id', this.memberId),
- memberType: this.memberType,
message: this.message,
userDeletionObstacles: this.userDeletionObstacles,
};
@@ -89,13 +67,10 @@ export default {
<template>
<gl-button
v-gl-tooltip
- variant="danger"
- :category="buttonCategory"
:title="title"
:aria-label="title"
- :icon="icon"
+ icon="remove"
data-qa-selector="delete_member_button"
@click="showRemoveMemberModal(modalData)"
- ><template v-if="buttonText">{{ buttonText }}</template></gl-button
- >
+ />
</template>
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
deleted file mode 100644
index 122e0a142a9..00000000000
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
-import { __, s__, sprintf } from '~/locale';
-import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
-import ActionButtonGroup from './action_button_group.vue';
-import LeaveButton from './leave_button.vue';
-import RemoveMemberButton from './remove_member_button.vue';
-
-export default {
- name: 'UserActionButtons',
- components: {
- ActionButtonGroup,
- RemoveMemberButton,
- LeaveButton,
- LdapOverrideButton: () =>
- import('ee_component/members/components/ldap/ldap_override_button.vue'),
- },
- props: {
- member: {
- type: Object,
- required: true,
- },
- isCurrentUser: {
- type: Boolean,
- required: true,
- },
- isInvitedUser: {
- type: Boolean,
- required: true,
- },
- permissions: {
- type: Object,
- required: true,
- },
- },
- computed: {
- message() {
- const { user, source } = this.member;
-
- if (user) {
- return sprintf(
- s__('Members|Are you sure you want to remove %{usersName} from "%{source}"?'),
- {
- usersName: user.name,
- source: source.fullName,
- },
- false,
- );
- }
-
- return sprintf(
- s__('Members|Are you sure you want to remove this orphaned member from "%{source}"?'),
- {
- source: source.fullName,
- },
- );
- },
- userDeletionObstaclesUserData() {
- return {
- name: this.member.user?.name,
- obstacles: parseUserDeletionObstacles(this.member.user),
- };
- },
- removeMemberButtonText() {
- return this.isInvitedUser ? null : __('Remove member');
- },
- removeMemberButtonIcon() {
- return this.isInvitedUser ? 'remove' : '';
- },
- removeMemberButtonCategory() {
- return this.isInvitedUser ? 'primary' : 'secondary';
- },
- },
-};
-</script>
-
-<template>
- <action-button-group>
- <div v-if="permissions.canRemove" class="gl-px-1">
- <leave-button v-if="isCurrentUser" :member="member" />
- <remove-member-button
- v-else
- :member-id="member.id"
- :member-type="member.type"
- :user-deletion-obstacles="userDeletionObstaclesUserData"
- :message="message"
- :icon="removeMemberButtonIcon"
- :button-text="removeMemberButtonText"
- :button-category="removeMemberButtonCategory"
- />
- </div>
- <div v-else-if="permissions.canOverride && !member.isOverridden" class="gl-px-1">
- <ldap-override-button :member="member" />
- </div>
- </action-button-group>
-</template>
diff --git a/app/assets/javascripts/members/components/action_dropdowns/constants.js b/app/assets/javascripts/members/components/action_dropdowns/constants.js
new file mode 100644
index 00000000000..8ccfc57dc28
--- /dev/null
+++ b/app/assets/javascripts/members/components/action_dropdowns/constants.js
@@ -0,0 +1,22 @@
+import { __, s__ } from '~/locale';
+
+export const I18N = {
+ actions: __('More actions'),
+ disableTwoFactor: s__('Members|Disable two-factor authentication'),
+ editPermissions: s__('Members|Edit permissions'),
+ leaveGroup: __('Leave group'),
+ removeMember: __('Remove member'),
+ confirmDisableTwoFactor: s__(
+ 'Members|Are you sure you want to disable the two-factor authentication for %{userName}?',
+ ),
+ confirmNormalUserRemoval: s__(
+ 'Members|Are you sure you want to remove %{userName} from "%{group}"?',
+ ),
+ confirmOrphanedUserRemoval: s__(
+ 'Members|Are you sure you want to remove this orphaned member from "%{group}"?',
+ ),
+ personalProjectOwnerCannotBeRemoved: s__("Members|A personal project's owner cannot be removed."),
+ lastGroupOwnerCannotBeRemoved: s__(
+ 'Members|A group must have at least one owner. To remove the member, assign a new owner.',
+ ),
+};
diff --git a/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue b/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue
new file mode 100644
index 00000000000..15606ad567c
--- /dev/null
+++ b/app/assets/javascripts/members/components/action_dropdowns/leave_group_dropdown_item.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlDropdownItem, GlModalDirective } from '@gitlab/ui';
+import { LEAVE_MODAL_ID } from '../../constants';
+import LeaveModal from '../modals/leave_modal.vue';
+
+export default {
+ name: 'LeaveGroupDropdownItem',
+ modalId: LEAVE_MODAL_ID,
+ components: {
+ GlDropdownItem,
+ LeaveModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ member: {
+ type: Object,
+ required: true,
+ },
+ permissions: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item v-gl-modal="$options.modalId">
+ <span class="gl-text-red-500">
+ <slot></slot>
+ </span>
+ <leave-modal :member="member" :permissions="permissions" />
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
new file mode 100644
index 00000000000..f224aaa31f7
--- /dev/null
+++ b/app/assets/javascripts/members/components/action_dropdowns/remove_member_dropdown_item.vue
@@ -0,0 +1,86 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+
+export default {
+ name: 'RemoveMemberDropdownItem',
+ components: { GlDropdownItem },
+ inject: ['namespace'],
+ props: {
+ memberId: {
+ type: Number,
+ required: true,
+ },
+ /**
+ * `GroupMember` (`app/models/members/group_member.rb`)
+ * or
+ * `ProjectMember` (`app/models/members/project_member.rb`).
+ */
+ memberModelType: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ modalMessage: {
+ type: String,
+ required: true,
+ },
+ isAccessRequest: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isInvite: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ userDeletionObstacles: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ preventRemoval: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapState({
+ memberPath(state) {
+ return state[this.namespace].memberPath;
+ },
+ }),
+ modalData() {
+ return {
+ isAccessRequest: this.isAccessRequest,
+ isInvite: this.isInvite,
+ memberPath: this.memberPath.replace(':id', this.memberId),
+ memberModelType: this.memberModelType,
+ message: this.modalMessage,
+ userDeletionObstacles: this.userDeletionObstacles,
+ preventRemoval: this.preventRemoval,
+ };
+ },
+ },
+ methods: {
+ ...mapActions({
+ showRemoveMemberModal(dispatch, payload) {
+ return dispatch(`${this.namespace}/showRemoveMemberModal`, payload);
+ },
+ }),
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item
+ data-qa-selector="delete_member_dropdown_item"
+ @click="showRemoveMemberModal(modalData)"
+ >
+ <span class="gl-text-red-500">
+ <slot></slot>
+ </span>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
new file mode 100644
index 00000000000..8f5c32956a2
--- /dev/null
+++ b/app/assets/javascripts/members/components/action_dropdowns/user_action_dropdown.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlDropdown, GlTooltipDirective } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
+import {
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '~/members/constants';
+import { I18N } from './constants';
+import LeaveGroupDropdownItem from './leave_group_dropdown_item.vue';
+import RemoveMemberDropdownItem from './remove_member_dropdown_item.vue';
+
+export default {
+ name: 'UserActionDropdown',
+ i18n: I18N,
+ components: {
+ GlDropdown,
+ DisableTwoFactorDropdownItem: () =>
+ import(
+ 'ee_component/members/components/action_dropdowns/disable_two_factor_dropdown_item.vue'
+ ),
+ LdapOverrideDropdownItem: () =>
+ import('ee_component/members/components/ldap/ldap_override_dropdown_item.vue'),
+ LeaveGroupDropdownItem,
+ RemoveMemberDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ member: {
+ type: Object,
+ required: true,
+ },
+ isCurrentUser: {
+ type: Boolean,
+ required: true,
+ },
+ permissions: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ modalDisableTwoFactor() {
+ const userName = this.member.user.username;
+ return sprintf(this.$options.i18n.confirmDisableTwoFactor, { userName }, false);
+ },
+ modalRemoveUser() {
+ const { user, source } = this.member;
+
+ if (this.permissions.canRemoveBlockedByLastOwner) {
+ if (this.member.type === MEMBER_MODEL_TYPE_PROJECT_MEMBER) {
+ return I18N.personalProjectOwnerCannotBeRemoved;
+ }
+
+ if (this.member.type === MEMBER_MODEL_TYPE_GROUP_MEMBER) {
+ return I18N.lastGroupOwnerCannotBeRemoved;
+ }
+ }
+
+ if (user) {
+ return sprintf(
+ this.$options.i18n.confirmNormalUserRemoval,
+ { userName: user.name, group: source.fullName },
+ false,
+ );
+ }
+
+ return sprintf(this.$options.i18n.confirmOrphanedUserRemoval, { group: source.fullName });
+ },
+ userDeletionObstaclesUserData() {
+ return {
+ name: this.member.user?.name,
+ obstacles: parseUserDeletionObstacles(this.member.user),
+ };
+ },
+ showDropdown() {
+ return (
+ this.permissions.canDisableTwoFactor || this.showLeaveOrRemove || this.showLdapOverride
+ );
+ },
+ showLeaveOrRemove() {
+ return this.permissions.canRemove || this.permissions.canRemoveBlockedByLastOwner;
+ },
+ showLdapOverride() {
+ return this.permissions.canOverride && !this.member.isOverridden;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ v-if="showDropdown"
+ v-gl-tooltip="$options.i18n.actions"
+ :text="$options.i18n.actions"
+ text-sr-only
+ icon="ellipsis_v"
+ category="tertiary"
+ no-caret
+ right
+ data-testid="user-action-dropdown"
+ data-qa-selector="user_action_dropdown"
+ >
+ <disable-two-factor-dropdown-item
+ v-if="permissions.canDisableTwoFactor"
+ :modal-message="modalDisableTwoFactor"
+ :user-id="member.user.id"
+ >
+ {{ $options.i18n.disableTwoFactor }}
+ </disable-two-factor-dropdown-item>
+
+ <template v-if="showLeaveOrRemove">
+ <leave-group-dropdown-item v-if="isCurrentUser" :member="member" :permissions="permissions">{{
+ $options.i18n.leaveGroup
+ }}</leave-group-dropdown-item>
+
+ <remove-member-dropdown-item
+ v-else
+ :member-id="member.id"
+ :member-model-type="member.type"
+ :user-deletion-obstacles="userDeletionObstaclesUserData"
+ :modal-message="modalRemoveUser"
+ :prevent-removal="permissions.canRemoveBlockedByLastOwner"
+ >{{ $options.i18n.removeMember }}</remove-member-dropdown-item
+ >
+ </template>
+
+ <ldap-override-dropdown-item v-else-if="showLdapOverride" :member="member">{{
+ $options.i18n.editPermissions
+ }}</ldap-override-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/members/components/modals/leave_modal.vue b/app/assets/javascripts/members/components/modals/leave_modal.vue
index e39669e17dd..8bc6aca9cc1 100644
--- a/app/assets/javascripts/members/components/modals/leave_modal.vue
+++ b/app/assets/javascripts/members/components/modals/leave_modal.vue
@@ -5,22 +5,30 @@ import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
-import { LEAVE_MODAL_ID } from '../../constants';
+import {
+ LEAVE_MODAL_ID,
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '../../constants';
export default {
name: 'LeaveModal',
actionCancel: {
text: __('Cancel'),
},
- actionPrimary: {
- text: __('Leave'),
- attributes: {
- variant: 'danger',
- },
- },
csrf,
modalId: LEAVE_MODAL_ID,
- modalContent: s__('Members|Are you sure you want to leave "%{source}"?'),
+ i18n: {
+ title: s__('Members|Leave "%{source}"'),
+ body: s__('Members|Are you sure you want to leave "%{source}"?'),
+ preventedTitle: s__('Members|Cannot leave "%{source}"'),
+ preventedBodyProjectMemberModelType: s__(
+ 'Members|You cannot remove yourself from a personal project.',
+ ),
+ preventedBodyGroupMemberModelType: s__(
+ 'Members|A group must have at least one owner. To leave this group, assign a new owner.',
+ ),
+ },
components: { GlModal, GlForm, GlSprintf, UserDeletionObstaclesList },
directives: {
GlTooltip: GlTooltipDirective,
@@ -31,6 +39,10 @@ export default {
type: Object,
required: true,
},
+ permissions: {
+ type: Object,
+ required: true,
+ },
},
computed: {
...mapState({
@@ -42,7 +54,35 @@ export default {
return this.memberPath.replace(/:id$/, 'leave');
},
modalTitle() {
- return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName });
+ return sprintf(
+ this.permissions.canRemoveBlockedByLastOwner
+ ? this.$options.i18n.preventedTitle
+ : this.$options.i18n.title,
+ { source: this.member.source.fullName },
+ );
+ },
+ preventedModalBody() {
+ if (this.member.type === MEMBER_MODEL_TYPE_PROJECT_MEMBER) {
+ return this.$options.i18n.preventedBodyProjectMemberModelType;
+ }
+
+ if (this.member.type === MEMBER_MODEL_TYPE_GROUP_MEMBER) {
+ return this.$options.i18n.preventedBodyGroupMemberModelType;
+ }
+
+ return null;
+ },
+ actionPrimary() {
+ if (this.permissions.canRemoveBlockedByLastOwner) {
+ return null;
+ }
+
+ return {
+ text: __('Leave'),
+ attributes: {
+ variant: 'danger',
+ },
+ };
},
obstacles() {
return parseUserDeletionObstacles(this.member.user);
@@ -64,13 +104,14 @@ export default {
v-bind="$attrs"
:modal-id="$options.modalId"
:title="modalTitle"
- :action-primary="$options.actionPrimary"
+ :action-primary="actionPrimary"
:action-cancel="$options.actionCancel"
@primary="handlePrimary"
>
<gl-form ref="form" :action="leavePath" method="post">
<p>
- <gl-sprintf :message="$options.modalContent">
+ <template v-if="permissions.canRemoveBlockedByLastOwner">{{ preventedModalBody }}</template>
+ <gl-sprintf v-else :message="$options.i18n.body">
<template #source>{{ member.source.fullName }}</template>
</gl-sprintf>
</p>
diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
index 1bb1f90302c..337379d8b4e 100644
--- a/app/assets/javascripts/members/components/modals/remove_member_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -4,6 +4,7 @@ import { mapActions, mapState } from 'vuex';
import csrf from '~/lib/utils/csrf';
import { s__, __ } from '~/locale';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
+import { MEMBER_MODEL_TYPE_GROUP_MEMBER } from '../../constants';
export default {
actionCancel: {
@@ -27,8 +28,13 @@ export default {
memberPath(state) {
return state[this.namespace].removeMemberModalData.memberPath;
},
- memberType(state) {
- return state[this.namespace].removeMemberModalData.memberType;
+ /**
+ * `GroupMember` (`app/models/members/group_member.rb`)
+ * or
+ * `ProjectMember` (`app/models/members/project_member.rb`).
+ */
+ memberModelType(state) {
+ return state[this.namespace].removeMemberModalData.memberModelType;
},
message(state) {
return state[this.namespace].removeMemberModalData.message;
@@ -36,12 +42,15 @@ export default {
userDeletionObstacles(state) {
return state[this.namespace].removeMemberModalData.userDeletionObstacles ?? {};
},
+ preventRemoval(state) {
+ return state[this.namespace].removeMemberModalData.preventRemoval;
+ },
removeMemberModalVisible(state) {
return state[this.namespace].removeMemberModalVisible;
},
}),
isGroupMember() {
- return this.memberType === 'GroupMember';
+ return this.memberModelType === MEMBER_MODEL_TYPE_GROUP_MEMBER;
},
actionText() {
if (this.isAccessRequest) {
@@ -53,6 +62,10 @@ export default {
return __('Remove member');
},
actionPrimary() {
+ if (this.preventRemoval) {
+ return null;
+ }
+
return {
text: this.actionText,
attributes: {
@@ -95,21 +108,22 @@ export default {
>
<form ref="form" :action="memberPath" method="post">
<p>{{ message }}</p>
+ <template v-if="!preventRemoval">
+ <user-deletion-obstacles-list
+ v-if="hasObstaclesToUserDeletion"
+ :obstacles="userDeletionObstacles.obstacles"
+ :user-name="userDeletionObstacles.name"
+ />
- <user-deletion-obstacles-list
- v-if="hasObstaclesToUserDeletion"
- :obstacles="userDeletionObstacles.obstacles"
- :user-name="userDeletionObstacles.name"
- />
-
- <input ref="method" type="hidden" name="_method" value="delete" />
- <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
- <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
- {{ __('Also remove direct user membership from subgroups and projects') }}
- </gl-form-checkbox>
- <gl-form-checkbox v-if="hasWorkspaceAccess" name="unassign_issuables">
- {{ __('Also unassign this user from related issues and merge requests') }}
- </gl-form-checkbox>
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
+ {{ __('Also remove direct user membership from subgroups and projects') }}
+ </gl-form-checkbox>
+ <gl-form-checkbox v-if="hasWorkspaceAccess" name="unassign_issuables">
+ {{ __('Also unassign this user from related issues and merge requests') }}
+ </gl-form-checkbox>
+ </template>
</form>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/members/components/table/created_at.vue b/app/assets/javascripts/members/components/table/created_at.vue
index 0bad70894f9..44d124ad0db 100644
--- a/app/assets/javascripts/members/components/table/created_at.vue
+++ b/app/assets/javascripts/members/components/table/created_at.vue
@@ -1,10 +1,10 @@
<script>
import { GlSprintf } from '@gitlab/ui';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import UserDate from '~/vue_shared/components/user_date.vue';
export default {
name: 'CreatedAt',
- components: { GlSprintf, TimeAgoTooltip },
+ components: { GlSprintf, UserDate },
props: {
date: {
type: String,
@@ -29,12 +29,12 @@ export default {
<span>
<gl-sprintf v-if="showCreatedBy" :message="s__('Members|%{time} by %{user}')">
<template #time>
- <time-ago-tooltip :time="date" />
+ <user-date :date="date" />
</template>
<template #user>
<a :href="createdBy.webUrl">{{ createdBy.name }}</a>
</template>
</gl-sprintf>
- <time-ago-tooltip v-else :time="date" />
+ <user-date v-else :date="date" />
</span>
</template>
diff --git a/app/assets/javascripts/members/components/table/member_action_buttons.vue b/app/assets/javascripts/members/components/table/member_action_buttons.vue
index ecc2ed82ad0..6ec7be608ba 100644
--- a/app/assets/javascripts/members/components/table/member_action_buttons.vue
+++ b/app/assets/javascripts/members/components/table/member_action_buttons.vue
@@ -3,12 +3,12 @@ import { MEMBER_TYPES, EE_ACTION_BUTTONS } from 'ee_else_ce/members/constants';
import AccessRequestActionButtons from '../action_buttons/access_request_action_buttons.vue';
import GroupActionButtons from '../action_buttons/group_action_buttons.vue';
import InviteActionButtons from '../action_buttons/invite_action_buttons.vue';
-import UserActionButtons from '../action_buttons/user_action_buttons.vue';
+import UserActionDropdown from '../action_dropdowns/user_action_dropdown.vue';
export default {
name: 'MemberActionButtons',
components: {
- UserActionButtons,
+ UserActionDropdown,
GroupActionButtons,
InviteActionButtons,
AccessRequestActionButtons,
@@ -32,15 +32,11 @@ export default {
type: Boolean,
required: true,
},
- isInvitedUser: {
- type: Boolean,
- required: true,
- },
},
computed: {
actionButtonComponent() {
const dictionary = {
- [MEMBER_TYPES.user]: 'user-action-buttons',
+ [MEMBER_TYPES.user]: 'user-action-dropdown',
[MEMBER_TYPES.group]: 'group-action-buttons',
[MEMBER_TYPES.invite]: 'invite-action-buttons',
[MEMBER_TYPES.accessRequest]: 'access-request-action-buttons',
@@ -60,6 +56,5 @@ export default {
:member="member"
:permissions="permissions"
:is-current-user="isCurrentUser"
- :is-invited-user="isInvitedUser"
/>
</template>
diff --git a/app/assets/javascripts/members/components/table/member_activity.vue b/app/assets/javascripts/members/components/table/member_activity.vue
new file mode 100644
index 00000000000..3b223cb1afa
--- /dev/null
+++ b/app/assets/javascripts/members/components/table/member_activity.vue
@@ -0,0 +1,38 @@
+<script>
+import UserDate from '~/vue_shared/components/user_date.vue';
+
+export default {
+ components: { UserDate },
+ props: {
+ member: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ userCreated() {
+ return this.member.user?.createdAt;
+ },
+ lastActivity() {
+ return this.member.user?.lastActivityOn;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="userCreated">
+ <strong>{{ s__('Members|User created') }}:</strong>
+ <user-date :date="userCreated" />
+ </div>
+ <div v-if="member.createdAt">
+ <strong>{{ s__('Members|Access granted') }}:</strong>
+ <user-date :date="member.createdAt" />
+ </div>
+ <div v-if="lastActivity">
+ <strong>{{ s__('Members|Last activity') }}:</strong>
+ <user-date :date="lastActivity" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/members/components/table/member_source.vue b/app/assets/javascripts/members/components/table/member_source.vue
index 30fcbfcd3f8..ed1971d020b 100644
--- a/app/assets/javascripts/members/components/table/member_source.vue
+++ b/app/assets/javascripts/members/components/table/member_source.vue
@@ -1,11 +1,19 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
export default {
name: 'MemberSource',
+ i18n: {
+ inherited: __('Inherited'),
+ directMember: __('Direct member'),
+ directMemberWithCreatedBy: s__('Members|Direct member by %{createdBy}'),
+ inheritedMemberWithCreatedBy: s__('Members|%{group} by %{createdBy}'),
+ },
directives: {
GlTooltip: GlTooltipDirective,
},
+ components: { GlSprintf },
props: {
memberSource: {
type: Object,
@@ -15,13 +23,40 @@ export default {
type: Boolean,
required: true,
},
+ createdBy: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ showCreatedBy() {
+ return this.createdBy?.name && this.createdBy?.webUrl;
+ },
+ messageWithCreatedBy() {
+ return this.isDirectMember
+ ? this.$options.i18n.directMemberWithCreatedBy
+ : this.$options.i18n.inheritedMemberWithCreatedBy;
+ },
},
};
</script>
<template>
- <span v-if="isDirectMember">{{ __('Direct member') }}</span>
- <a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{
+ <span v-if="showCreatedBy">
+ <gl-sprintf :message="messageWithCreatedBy">
+ <template #group>
+ <a v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
+ memberSource.fullName
+ }}</a>
+ </template>
+ <template #createdBy>
+ <a :href="createdBy.webUrl">{{ createdBy.name }}</a>
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-else-if="isDirectMember">{{ $options.i18n.directMember }}</span>
+ <a v-else v-gl-tooltip.hover="$options.i18n.inherited" :href="memberSource.webUrl">{{
memberSource.fullName
}}</a>
</template>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 0512bc04085..8f03a298e63 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -2,14 +2,20 @@
import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
-import { canUnban, canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
+import {
+ canDisableTwoFactor,
+ canUnban,
+ canOverride,
+ canRemove,
+ canRemoveBlockedByLastOwner,
+ canResend,
+ canUpdate,
+} from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import UserDate from '~/vue_shared/components/user_date.vue';
import {
FIELD_KEY_ACTIONS,
FIELDS,
ACTIVE_TAB_QUERY_PARAM_NAME,
- TAB_QUERY_PARAM_VALUES,
MEMBER_STATE_AWAITING,
MEMBER_STATE_ACTIVE,
USER_STATE_BLOCKED,
@@ -23,6 +29,7 @@ import ExpirationDatepicker from './expiration_datepicker.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
+import MemberActivity from './member_activity.vue';
import RoleDropdown from './role_dropdown.vue';
export default {
@@ -40,11 +47,13 @@ export default {
RemoveGroupLinkModal,
RemoveMemberModal,
ExpirationDatepicker,
- UserDate,
+ MemberActivity,
+ DisableTwoFactorModal: () =>
+ import('ee_component/members/components/modals/disable_two_factor_modal.vue'),
LdapOverrideConfirmationModal: () =>
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
- inject: ['namespace', 'currentUserId'],
+ inject: ['namespace', 'currentUserId', 'canManageMembers'],
props: {
tabQueryParamValue: {
type: String,
@@ -80,18 +89,17 @@ export default {
return paramName && currentPage && perPage && totalItems;
},
- isInvitedUser() {
- return this.tabQueryParamValue === TAB_QUERY_PARAM_VALUES.invite;
- },
},
methods: {
hasActionButtons(member) {
return (
canRemove(member) ||
+ canRemoveBlockedByLastOwner(member, this.canManageMembers) ||
canResend(member) ||
canUpdate(member, this.currentUserId) ||
canOverride(member) ||
- canUnban(member)
+ canUnban(member) ||
+ canDisableTwoFactor(member)
);
},
showField(field) {
@@ -249,7 +257,11 @@ export default {
<template #cell(source)="{ item: member }">
<members-table-cell #default="{ isDirectMember }" :member="member">
- <member-source :is-direct-member="isDirectMember" :member-source="member.source" />
+ <member-source
+ :is-direct-member="isDirectMember"
+ :member-source="member.source"
+ :created-by="member.createdBy"
+ />
</members-table-cell>
</template>
@@ -281,12 +293,8 @@ export default {
</members-table-cell>
</template>
- <template #cell(userCreatedAt)="{ item: member }">
- <user-date :date="member.user.createdAt" />
- </template>
-
- <template #cell(lastActivityOn)="{ item: member }">
- <user-date :date="member.user.lastActivityOn" />
+ <template #cell(activity)="{ item: member }">
+ <member-activity :member="member" />
</template>
<template #cell(actions)="{ item: member }">
@@ -294,7 +302,6 @@ export default {
<member-action-buttons
:member-type="memberType"
:is-current-user="isCurrentUser"
- :is-invited-user="isInvitedUser"
:permissions="permissions"
:member="member"
/>
@@ -317,6 +324,7 @@ export default {
:label-prev-page="__('Go to previous page')"
align="center"
/>
+ <disable-two-factor-modal />
<remove-group-link-modal />
<remove-member-modal />
<ldap-override-confirmation-modal />
diff --git a/app/assets/javascripts/members/components/table/members_table_cell.vue b/app/assets/javascripts/members/components/table/members_table_cell.vue
index 51eff428d63..407cbc55dd3 100644
--- a/app/assets/javascripts/members/components/table/members_table_cell.vue
+++ b/app/assets/javascripts/members/components/table/members_table_cell.vue
@@ -5,13 +5,14 @@ import {
isDirectMember,
isCurrentUser,
canRemove,
+ canRemoveBlockedByLastOwner,
canResend,
canUpdate,
} from '../../utils';
export default {
name: 'MembersTableCell',
- inject: ['currentUserId'],
+ inject: ['currentUserId', 'canManageMembers'],
props: {
member: {
type: Object,
@@ -45,6 +46,9 @@ export default {
isCurrentUser() {
return isCurrentUser(this.member, this.currentUserId);
},
+ canRemoveBlockedByLastOwner() {
+ return canRemoveBlockedByLastOwner(this.member, this.canManageMembers);
+ },
canRemove() {
return canRemove(this.member);
},
@@ -62,6 +66,7 @@ export default {
isCurrentUser: this.isCurrentUser,
permissions: {
canRemove: this.canRemove,
+ canRemoveBlockedByLastOwner: this.canRemoveBlockedByLastOwner,
canResend: this.canResend,
canUpdate: this.canUpdate,
},
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
index 6cd8bf57313..70808587d56 100644
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ b/app/assets/javascripts/members/components/table/role_dropdown.vue
@@ -2,7 +2,9 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { mapActions } from 'vuex';
+import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
+import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
export default {
name: 'RoleDropdown',
@@ -11,7 +13,7 @@ export default {
GlDropdownItem,
LdapDropdownItem: () => import('ee_component/members/components/ldap/ldap_dropdown_item.vue'),
},
- inject: ['namespace'],
+ inject: ['namespace', 'group'],
props: {
member: {
type: Object,
@@ -30,7 +32,7 @@ export default {
},
computed: {
disabled() {
- return this.busy || (this.permissions.canOverride && !this.member.isOverridden);
+ return this.permissions.canOverride && !this.member.isOverridden;
},
},
mounted() {
@@ -50,22 +52,45 @@ export default {
return dispatch(`${this.namespace}/updateMemberRole`, payload);
},
}),
- handleSelect(value, name) {
- if (value === this.member.accessLevel.integerValue) {
+ async handleOverageConfirm(currentRoleValue, newRoleValue, newRoleName) {
+ return guestOverageConfirmAction({
+ currentRoleValue,
+ newRoleValue,
+ newRoleName,
+ group: this.group,
+ memberId: this.member.id,
+ memberType: this.namespace,
+ });
+ },
+ async handleSelect(newRoleValue, newRoleName) {
+ const currentRoleValue = this.member.accessLevel.integerValue;
+ if (newRoleValue === currentRoleValue) {
return;
}
this.busy = true;
+ const confirmed = await this.handleOverageConfirm(
+ currentRoleValue,
+ newRoleValue,
+ newRoleName,
+ );
+ if (!confirmed) {
+ this.busy = false;
+ return;
+ }
+
this.updateMemberRole({
memberId: this.member.id,
- accessLevel: { integerValue: value, stringValue: name },
+ accessLevel: { integerValue: newRoleValue, stringValue: newRoleName },
})
.then(() => {
this.$toast.show(s__('Members|Role updated successfully.'));
- this.busy = false;
})
- .catch(() => {
+ .catch((error) => {
+ Sentry.captureException(error);
+ })
+ .finally(() => {
this.busy = false;
});
},
@@ -80,6 +105,7 @@ export default {
:text="member.accessLevel.stringValue"
:header-text="__('Change role')"
:disabled="disabled"
+ :loading="busy"
>
<gl-dropdown-item
v-for="(value, name) in member.validRoles"
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index dab544c7cbc..68c5831db62 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -20,6 +20,7 @@ export const FIELD_KEY_MAX_ROLE = 'maxRole';
export const FIELD_KEY_USER_CREATED_AT = 'userCreatedAt';
export const FIELD_KEY_LAST_ACTIVITY_ON = 'lastActivityOn';
export const FIELD_KEY_EXPIRATION = 'expiration';
+export const FIELD_KEY_ACTIVITY = 'activity';
export const FIELD_KEY_LAST_SIGN_IN = 'lastSignIn';
export const FIELD_KEY_ACTIONS = 'actions';
@@ -41,8 +42,6 @@ export const FIELDS = [
{
key: FIELD_KEY_GRANTED,
label: __('Access granted'),
- thClass: 'col-meta',
- tdClass: 'col-meta',
sort: {
asc: 'last_joined',
desc: 'oldest_joined',
@@ -77,8 +76,14 @@ export const FIELDS = [
tdClass: 'col-expiration',
},
{
+ key: FIELD_KEY_ACTIVITY,
+ label: s__('Members|Activity'),
+ thClass: 'col-activity',
+ tdClass: 'col-activity',
+ },
+ {
key: FIELD_KEY_USER_CREATED_AT,
- label: __('Created on'),
+ label: s__('Members|User created'),
sort: {
asc: 'oldest_created_user',
desc: 'recent_created_user',
@@ -158,6 +163,12 @@ export const MEMBER_TYPES = {
accessRequest: 'accessRequest',
};
+// `app/models/members/group_member.rb`
+export const MEMBER_MODEL_TYPE_GROUP_MEMBER = 'GroupMember';
+
+// `app/models/members/project_member.rb`
+export const MEMBER_MODEL_TYPE_PROJECT_MEMBER = 'ProjectMember';
+
export const TAB_QUERY_PARAM_VALUES = {
group: 'groups',
invite: 'invited',
diff --git a/app/assets/javascripts/members/guest_overage_confirm_action.js b/app/assets/javascripts/members/guest_overage_confirm_action.js
new file mode 100644
index 00000000000..2205c3ad792
--- /dev/null
+++ b/app/assets/javascripts/members/guest_overage_confirm_action.js
@@ -0,0 +1,3 @@
+export const guestOverageConfirmAction = () => {
+ return true;
+};
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 359239c5c0c..c7398127727 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -21,6 +21,8 @@ export const initMembersApp = (el, options) => {
canExportMembers,
canFilterByEnterprise,
exportCsvPath,
+ groupName,
+ groupPath,
...vuexStoreAttributes
} = parseDataAttributes(el);
@@ -66,6 +68,10 @@ export const initMembersApp = (el, options) => {
canFilterByEnterprise,
canExportMembers,
exportCsvPath,
+ group: {
+ name: groupName,
+ path: groupPath,
+ },
},
render: (createElement) => createElement('members-tabs'),
});
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index bf87ab53d36..09e4b5e8a6f 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -51,6 +51,9 @@ export const canRemove = (member) => {
return isDirectMember(member) && member.canRemove;
};
+export const canRemoveBlockedByLastOwner = (member, canManageMembers) =>
+ isDirectMember(member) && canManageMembers && member.isLastOwner;
+
export const canResend = (member) => {
return Boolean(member.invite?.canResend);
};
@@ -106,6 +109,9 @@ export const buildSortHref = ({
};
// Defined in `ee/app/assets/javascripts/members/utils.js`
+export const canDisableTwoFactor = () => false;
+
+// Defined in `ee/app/assets/javascripts/members/utils.js`
export const canOverride = () => false;
// Defined in `ee/app/assets/javascripts/members/utils.js`
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 5a1410ceeba..46ee8fecfc5 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,4 +1,4 @@
-/* eslint-disable no-new, class-methods-use-this */
+/* eslint-disable class-methods-use-this */
import $ from 'jquery';
import Vue from 'vue';
import { createAlert } from '~/flash';
@@ -134,8 +134,8 @@ function destroyPipelines(app) {
return null;
}
-function loadDiffs({ url, sticky }) {
- return axios.get(`${url}.json${location.search}`).then(({ data }) => {
+function loadDiffs({ url, sticky, tabs }) {
+ return axios.get(url).then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
initDiffStatsDropdown(sticky);
@@ -143,7 +143,9 @@ function loadDiffs({ url, sticky }) {
localTimeAgo(document.querySelectorAll('#diffs .js-timeago'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
- new Diff();
+ tabs.createDiff();
+ tabs.setHubToDiff();
+
scrollToContainer('#diffs');
$('.diff-file').each((i, el) => {
@@ -204,6 +206,7 @@ export default class MergeRequestTabs {
this.currentTab = null;
this.diffsLoaded = false;
+ this.diffsClass = null;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.eventHub = createEventHub();
@@ -211,6 +214,7 @@ export default class MergeRequestTabs {
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
+ this.switchViewType = this.switchViewType.bind(this);
this.tabShown = this.tabShown.bind(this);
this.clickTab = this.clickTab.bind(this);
@@ -230,11 +234,13 @@ export default class MergeRequestTabs {
this.tabShown(action, location.href);
this.eventHub.$emit('MergeRequestTabChange', action);
});
+ this.eventHub.$on('diff:switch-view-type', this.switchViewType);
}
// Used in tests
unbindEvents() {
$('.merge-request-tabs a[data-toggle="tabvue"]').off('click', this.clickTab);
+ this.eventHub.$off('diff:switch-view-type', this.switchViewType);
}
storeScroll() {
@@ -341,7 +347,7 @@ export default class MergeRequestTabs {
in practice, this only occurs when comparing commits in
the new merge request form page.
*/
- this.loadDiff(href);
+ this.loadDiff({ endpoint: href, strip: true });
}
// this.hideSidebar();
this.expandViewContainer();
@@ -503,17 +509,20 @@ export default class MergeRequestTabs {
}
// load the diff tab content from the backend
- loadDiff(source) {
+ loadDiff({ endpoint, strip = true }) {
if (this.diffsLoaded) {
document.dispatchEvent(new CustomEvent('scroll'));
return;
}
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ const diffUrl = strip ? `${parseUrlPathname(endpoint)}.json${location.search}` : endpoint;
+
loadDiffs({
- // We extract pathname for the current Changes tab anchor href
- // some pages like MergeRequestsController#new has query parameters on that anchor
- url: parseUrlPathname(source),
+ url: diffUrl,
sticky: computeTopOffset(this.mergeRequestTabs),
+ tabs: this,
})
.then(() => {
if (this.isDiffAction(this.currentAction)) {
@@ -528,6 +537,21 @@ export default class MergeRequestTabs {
});
});
}
+ switchViewType({ source }) {
+ this.diffsLoaded = false;
+
+ this.loadDiff({ endpoint: source, strip: false });
+ }
+ createDiff() {
+ if (!this.diffsClass) {
+ this.diffsClass = new Diff({ mergeRequestEventHub: this.eventHub });
+ }
+ }
+ setHubToDiff() {
+ if (this.diffsClass) {
+ this.diffsClass.mrHub = this.eventHub;
+ }
+ }
diffViewType() {
return $('.js-diff-view-buttons button.active').data('viewType');
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index 4a675cf7563..6af1baaa37e 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -56,11 +56,7 @@ export default {
},
watch: {
discussionTabCounter(val) {
- if (this.glFeatures.paginatedMrDiscussions) {
- if (this.doneFetchingBatchDiscussions) {
- this.discussionCounter = val;
- }
- } else {
+ if (this.doneFetchingBatchDiscussions) {
this.discussionCounter = val;
}
},
@@ -86,8 +82,8 @@ export default {
@disappear="setStickyHeaderVisible(true)"
>
<div
- v-if="isStickyHeaderVisible"
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-white gl-border-1 gl-border-b-solid gl-border-b-gray-100 gl-pt-3 gl-display-none gl-md-display-block"
+ :class="{ 'gl-visibility-hidden': !isStickyHeaderVisible }"
>
<div
class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-px-5"
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
index 5f54f24e24c..0bb2a913dec 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
@@ -19,6 +19,25 @@ export default {
artifactsLabel: __('Artifacts'),
parametersLabel: __('Parameters'),
metricsLabel: __('Metrics'),
+ metadataLabel: __('Metadata'),
+ },
+ computed: {
+ sections() {
+ return [
+ {
+ sectionName: this.$options.i18n.parametersLabel,
+ sectionValues: this.candidate.params,
+ },
+ {
+ sectionName: this.$options.i18n.metricsLabel,
+ sectionValues: this.candidate.metrics,
+ },
+ {
+ sectionName: this.$options.i18n.metadataLabel,
+ sectionValues: this.candidate.metadata,
+ },
+ ];
+ },
},
};
</script>
@@ -67,27 +86,18 @@ export default {
</td>
</tr>
- <tr class="divider"></tr>
-
- <tr v-for="(param, index) in candidate.params" :key="param.name">
- <td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
- {{ $options.i18n.parametersLabel }}
- </td>
- <td v-else></td>
- <td class="gl-font-weight-bold">{{ param.name }}</td>
- <td>{{ param.value }}</td>
- </tr>
+ <template v-for="{ sectionName, sectionValues } in sections">
+ <tr :key="sectionName" class="divider"></tr>
- <tr class="divider"></tr>
-
- <tr v-for="(metric, index) in candidate.metrics" :key="metric.name">
- <td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
- {{ $options.i18n.metricsLabel }}
- </td>
- <td v-else></td>
- <td class="gl-font-weight-bold">{{ metric.name }}</td>
- <td>{{ metric.value }}</td>
- </tr>
+ <tr v-for="(item, index) in sectionValues" :key="item.name">
+ <td v-if="index === 0" class="gl-text-secondary gl-font-weight-bold">
+ {{ sectionName }}
+ </td>
+ <td v-else></td>
+ <td class="gl-font-weight-bold">{{ item.name }}</td>
+ <td>{{ item.value }}</td>
+ </tr>
+ </template>
</tbody>
</table>
</div>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
index f8e269d3b57..5d13122765a 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
@@ -1,6 +1,8 @@
<script>
-import { GlTable, GlLink } from '@gitlab/ui';
+import { GlTable, GlLink, GlPagination, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
+import { getParameterValues, setUrlParams } from '~/lib/utils/url_utility';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import IncubationAlert from './incubation_alert.vue';
export default {
@@ -8,24 +10,55 @@ export default {
components: {
GlTable,
GlLink,
+ TimeAgo,
IncubationAlert,
+ GlPagination,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['candidates', 'metricNames', 'paramNames', 'pagination'],
+ data() {
+ return {
+ page: parseInt(getParameterValues('page')[0], 10) || 1,
+ };
},
- inject: ['candidates', 'metricNames', 'paramNames'],
computed: {
fields() {
return [
+ { key: 'name', label: this.$options.i18n.nameLabel },
+ { key: 'created_at', label: this.$options.i18n.createdAtLabel },
+ { key: 'user', label: this.$options.i18n.userLabel },
...this.paramNames,
...this.metricNames,
{ key: 'details', label: '' },
{ key: 'artifact', label: '' },
];
},
+ displayPagination() {
+ return this.candidates.length > 0;
+ },
+ prevPage() {
+ return this.pagination.page > 1 ? this.pagination.page - 1 : null;
+ },
+ nextPage() {
+ return !this.pagination.isLastPage ? this.pagination.page + 1 : null;
+ },
+ },
+ methods: {
+ generateLink(page) {
+ return setUrlParams({ page });
+ },
},
i18n: {
titleLabel: __('Experiment candidates'),
emptyStateLabel: __('This experiment has no logged candidates'),
artifactsLabel: __('Artifacts'),
detailsLabel: __('Details'),
+ userLabel: __('User'),
+ createdAtLabel: __('Created at'),
+ nameLabel: __('Name'),
+ noDataContent: __('-'),
},
};
</script>
@@ -43,17 +76,59 @@ export default {
:items="candidates"
:empty-text="$options.i18n.emptyStateLabel"
show-empty
- class="gl-mt-0!"
+ small
+ class="gl-mt-0! ml-candidate-table"
>
+ <template #cell()="data">
+ <div v-gl-tooltip.hover :title="data.value">{{ data.value }}</div>
+ </template>
+
<template #cell(artifact)="data">
- <gl-link v-if="data.value" :href="data.value" target="_blank">{{
- $options.i18n.artifactsLabel
- }}</gl-link>
+ <gl-link
+ v-if="data.value"
+ v-gl-tooltip.hover
+ :href="data.value"
+ target="_blank"
+ :title="$options.i18n.artifactsLabel"
+ >{{ $options.i18n.artifactsLabel }}</gl-link
+ >
+ <div v-else v-gl-tooltip.hover :title="$options.i18n.artifactsLabel">
+ {{ $options.i18n.noDataContent }}
+ </div>
</template>
<template #cell(details)="data">
- <gl-link :href="data.value">{{ $options.i18n.detailsLabel }}</gl-link>
+ <gl-link v-gl-tooltip.hover :href="data.value" :title="$options.i18n.detailsLabel">{{
+ $options.i18n.detailsLabel
+ }}</gl-link>
+ </template>
+
+ <template #cell(created_at)="data">
+ <time-ago v-gl-tooltip.hover :time="data.value" :title="data.value" />
+ </template>
+
+ <template #cell(user)="data">
+ <gl-link
+ v-if="data.value"
+ v-gl-tooltip.hover
+ :href="data.value.path"
+ :title="data.value.username"
+ >@{{ data.value.username }}</gl-link
+ >
+ <div v-else>{{ $options.i18n.noDataContent }}</div>
</template>
</gl-table>
+
+ <gl-pagination
+ v-if="displayPagination"
+ v-model="pagination.page"
+ :prev-page="prevPage"
+ :next-page="nextPage"
+ :total-items="pagination.totalItems"
+ :per-page="pagination.perPage"
+ :link-gen="generateLink"
+ align="center"
+ class="w-100"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
index 8b65eec051f..29786a79c56 100644
--- a/app/assets/javascripts/monitoring/requests/index.js
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -1,7 +1,9 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes, {
+import {
+ HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { PROMETHEUS_TIMEOUT } from '../constants';
@@ -36,9 +38,9 @@ export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
// https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
const { response = {} } = error;
if (
- response.status === statusCodes.BAD_REQUEST ||
+ response.status === HTTP_STATUS_BAD_REQUEST ||
response.status === HTTP_STATUS_UNPROCESSABLE_ENTITY ||
- response.status === statusCodes.SERVICE_UNAVAILABLE
+ response.status === HTTP_STATUS_SERVICE_UNAVAILABLE
) {
const { data } = response;
if (data?.status === 'error' && data?.error) {
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index e0eaf76b5f6..5fab292b6df 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,7 +1,7 @@
import { pick } from 'lodash';
import Vue from 'vue';
import { BACKOFF_TIMEOUT } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
import * as types from './mutation_types';
import { mapToDashboardViewModel, mapPanelToViewModel, normalizeQueryResponseData } from './utils';
@@ -43,9 +43,9 @@ const emptyStateFromError = (error) => {
// Axios error responses
const { response } = error;
- if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
+ if (response && response.status === HTTP_STATUS_SERVICE_UNAVAILABLE) {
return metricStates.CONNECTION_FAILED;
- } else if (response && response.status === httpStatusCodes.BAD_REQUEST) {
+ } else if (response && response.status === HTTP_STATUS_BAD_REQUEST) {
// Note: "error.response.data.error" may contain Prometheus error information
return metricStates.BAD_QUERY;
}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index e10605609b0..f5f10aa4a9b 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { parseBoolean } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
import notesEventHub from '~/notes/event_hub';
@@ -9,6 +10,13 @@ import { getNotesFilterData } from '../notes/utils/get_notes_filter_data';
import initWidget from '../vue_merge_request_widget';
export default () => {
+ requestIdleCallback(
+ () => {
+ renderGFM(document.getElementById('diff-notes-app'));
+ },
+ { timeout: 500 },
+ );
+
const el = document.getElementById('js-vue-mr-discussions');
if (!el) {
return;
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
index ef59140115d..7b0076cc5d4 100644
--- a/app/assets/javascripts/nav/components/new_nav_toggle.vue
+++ b/app/assets/javascripts/nav/components/new_nav_toggle.vue
@@ -3,6 +3,7 @@ import { GlBadge, GlToggle } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
export default {
i18n: {
@@ -34,9 +35,19 @@ export default {
};
},
methods: {
- async toggleNav() {
+ toggleNav() {
+ this.isEnabled = !this.isEnabled;
+ this.updateAndReload();
+ },
+ async updateAndReload() {
try {
- await axios.put(this.endpoint, { user: { use_new_navigation: !this.enabled } });
+ await axios.put(this.endpoint, { user: { use_new_navigation: this.isEnabled } });
+
+ Tracking.event(undefined, 'click_toggle', {
+ label: this.enabled ? 'disable_new_nav_beta' : 'enable_new_nav_beta',
+ property: 'navigation',
+ });
+
window.location.reload();
} catch (error) {
createAlert({
@@ -55,17 +66,15 @@ export default {
class="gl-px-4 gl-py-2 gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<b>{{ $options.i18n.sectionTitle }}</b>
- <gl-badge>{{ $options.i18n.badgeLabel }}</gl-badge>
+ <gl-badge variant="info">{{ $options.i18n.badgeLabel }}</gl-badge>
</div>
- <div class="menu-item gl-display-flex! gl-justify-content-space-between gl-align-items-center">
+ <div
+ class="menu-item gl-cursor-pointer gl-display-flex! gl-justify-content-space-between gl-align-items-center"
+ @click.prevent.stop="toggleNav"
+ >
{{ $options.i18n.toggleMenuItemLabel }}
- <gl-toggle
- v-model="isEnabled"
- :label="$options.i18n.toggleLabel"
- label-position="hidden"
- @change="toggleNav"
- />
+ <gl-toggle :value="isEnabled" :label="$options.i18n.toggleLabel" label-position="hidden" />
</div>
</li>
</template>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 2ccb9a0b514..c6e7117cf2e 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -312,7 +312,7 @@ export default {
if (this.isLoggedIn) {
const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
- this.autosave = new Autosave($(this.$refs.textarea), [
+ this.autosave = new Autosave(this.$refs.textarea, [
this.$options.i18n.note,
noteableType,
this.getNoteableData.id,
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 36f7d720e48..79b6139d4b1 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,6 +1,7 @@
<script>
import { GlIcon, GlBadge, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -74,6 +75,12 @@ export default {
};
},
computed: {
+ authorId() {
+ return getIdFromGraphQLId(this.author.id);
+ },
+ authorHref() {
+ return this.author.path || this.author.webUrl;
+ },
toggleChevronIconName() {
return this.expanded ? 'chevron-up' : 'chevron-down';
},
@@ -145,9 +152,9 @@ export default {
<template v-if="hasAuthor">
<a
ref="authorNameLink"
- :href="author.path"
+ :href="authorHref"
:class="authorLinkClasses"
- :data-user-id="author.id"
+ :data-user-id="authorId"
:data-username="author.username"
>
<span class="note-header-author-name gl-font-weight-bold">
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index 61cb4ab2a10..17272d5abef 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { s__ } from '~/locale';
import Autosave from '~/autosave';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
@@ -16,7 +15,7 @@ export default {
keys = keys.concat(extraKeys);
}
- this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), keys);
+ this.autosave = new Autosave(this.$refs.noteForm.$refs.textarea, keys);
},
resetAutoSave() {
this.autosave.reset();
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index d290a8ccb84..5cad091ce2c 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -101,7 +101,7 @@ export const fetchDiscussions = (
if (
getters.noteableType === constants.ISSUE_NOTEABLE_TYPE ||
- window.gon?.features?.paginatedMrDiscussions
+ getters.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
) {
return dispatch('fetchDiscussionsBatch', { path, config, perPage: 20 });
}
diff --git a/app/assets/javascripts/notifications/components/notification_email_listbox_input.vue b/app/assets/javascripts/notifications/components/notification_email_listbox_input.vue
new file mode 100644
index 00000000000..5d5524deb0d
--- /dev/null
+++ b/app/assets/javascripts/notifications/components/notification_email_listbox_input.vue
@@ -0,0 +1,46 @@
+<script>
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+
+export default {
+ components: {
+ ListboxInput,
+ },
+ inject: ['label', 'name', 'emails', 'emptyValueText', 'value', 'disabled'],
+ data() {
+ return {
+ selected: this.value,
+ };
+ },
+ computed: {
+ options() {
+ return [
+ {
+ value: '',
+ text: this.emptyValueText,
+ },
+ ...this.emails.map((email) => ({
+ text: email,
+ value: email,
+ })),
+ ];
+ },
+ },
+ methods: {
+ async onSelect() {
+ await this.$nextTick();
+ this.$el.closest('form').submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <listbox-input
+ v-model="selected"
+ :label="label"
+ :name="name"
+ :items="options"
+ :disabled="disabled"
+ @select="onSelect"
+ />
+</template>
diff --git a/app/assets/javascripts/notifications/index.js b/app/assets/javascripts/notifications/index.js
index a81f2c2590b..1395084f68c 100644
--- a/app/assets/javascripts/notifications/index.js
+++ b/app/assets/javascripts/notifications/index.js
@@ -2,10 +2,37 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotificationsDropdown from './components/notifications_dropdown.vue';
+import NotificationEmailListboxInput from './components/notification_email_listbox_input.vue';
Vue.use(GlToast);
+const initNotificationEmailListboxInputs = () => {
+ const els = [...document.querySelectorAll('.js-notification-email-listbox-input')];
+
+ els.forEach((el, index) => {
+ const { label, name, emptyValueText, value = '' } = el.dataset;
+
+ return new Vue({
+ el,
+ name: `NotificationEmailListboxInputRoot${index + 1}`,
+ provide: {
+ label,
+ name,
+ emails: JSON.parse(el.dataset.emails),
+ emptyValueText,
+ value,
+ disabled: parseBoolean(el.dataset.disabled),
+ },
+ render(h) {
+ return h(NotificationEmailListboxInput);
+ },
+ });
+ });
+};
+
export default () => {
+ initNotificationEmailListboxInputs();
+
const containers = document.querySelectorAll('.js-vue-notification-dropdown');
if (!containers.length) return false;
diff --git a/app/assets/javascripts/observability/components/observability_app.vue b/app/assets/javascripts/observability/components/observability_app.vue
index 33d23ea043b..ff9cf6ff6c5 100644
--- a/app/assets/javascripts/observability/components/observability_app.vue
+++ b/app/assets/javascripts/observability/components/observability_app.vue
@@ -2,7 +2,7 @@
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { setUrlParams } from '~/lib/utils/url_utility';
-import { MESSAGE_EVENT_TYPE, OBSERVABILITY_ROUTES, SKELETON_VARIANT } from '../constants';
+import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '../constants';
import ObservabilitySkeleton from './skeleton/index.vue';
export default {
@@ -23,16 +23,16 @@ export default {
);
},
getSkeletonVariant() {
- switch (this.$route.path) {
- case OBSERVABILITY_ROUTES.DASHBOARDS:
- return SKELETON_VARIANT.DASHBOARDS;
- case OBSERVABILITY_ROUTES.EXPLORE:
- return SKELETON_VARIANT.EXPLORE;
- case OBSERVABILITY_ROUTES.MANAGE:
- return SKELETON_VARIANT.MANAGE;
- default:
- return SKELETON_VARIANT.DASHBOARDS;
- }
+ const [, variant] =
+ Object.entries(SKELETON_VARIANTS_BY_ROUTE).find(([path]) =>
+ this.$route.path.endsWith(path),
+ ) || [];
+
+ const DEFAULT_SKELETON = 'dashboards';
+
+ if (!variant) return DEFAULT_SKELETON;
+
+ return variant;
},
},
mounted() {
@@ -51,7 +51,7 @@ export default {
} = e;
switch (type) {
case MESSAGE_EVENT_TYPE.GOUI_LOADED:
- this.$refs.iframeSkeleton.handleSkeleton();
+ this.$refs.observabilitySkeleton.onContentLoaded();
break;
case MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE:
this.routeUpdateHandler(payload);
@@ -80,7 +80,7 @@ export default {
</script>
<template>
- <observability-skeleton ref="iframeSkeleton" :variant="getSkeletonVariant">
+ <observability-skeleton ref="observabilitySkeleton" :variant="getSkeletonVariant">
<iframe
id="observability-ui-iframe"
data-testid="observability-ui-iframe"
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/skeleton/index.vue
index 1e2671c8166..c8f196a43f4 100644
--- a/app/assets/javascripts/observability/components/skeleton/index.vue
+++ b/app/assets/javascripts/observability/components/skeleton/index.vue
@@ -1,17 +1,32 @@
<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-import { SKELETON_VARIANT } from '../../constants';
+import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
+
+import {
+ SKELETON_VARIANTS_BY_ROUTE,
+ SKELETON_STATE,
+ DEFAULT_TIMERS,
+ OBSERVABILITY_ROUTES,
+ TIMEOUT_ERROR_LABEL,
+ TIMEOUT_ERROR_MESSAGE,
+} from '../../constants';
import DashboardsSkeleton from './dashboards.vue';
import ExploreSkeleton from './explore.vue';
import ManageSkeleton from './manage.vue';
export default {
- SKELETON_VARIANT,
components: {
GlSkeletonLoader,
DashboardsSkeleton,
ExploreSkeleton,
ManageSkeleton,
+ GlAlert,
+ },
+ SKELETON_VARIANTS_BY_ROUTE,
+ SKELETON_STATE,
+ OBSERVABILITY_ROUTES,
+ i18n: {
+ TIMEOUT_ERROR_LABEL,
+ TIMEOUT_ERROR_MESSAGE,
},
props: {
variant: {
@@ -22,65 +37,94 @@ export default {
},
data() {
return {
- loading: null,
- timerId: null,
+ state: null,
+ loadingTimeout: null,
+ errorTimeout: null,
};
},
mounted() {
- this.timerId = setTimeout(() => {
- /**
- * If observability UI is not loaded then this.loading would be null
- * we will show skeleton in that case
- */
- if (this.loading !== false) {
- this.showSkeleton();
- }
- }, 500);
+ this.setLoadingTimeout();
+ this.setErrorTimeout();
+ },
+ destroyed() {
+ clearTimeout(this.loadingTimeout);
+ clearTimeout(this.errorTimeout);
},
methods: {
- handleSkeleton() {
- if (this.loading === null) {
+ onContentLoaded() {
+ clearTimeout(this.errorTimeout);
+ clearTimeout(this.loadingTimeout);
+
+ this.hideSkeleton();
+ },
+ setLoadingTimeout() {
+ this.loadingTimeout = setTimeout(() => {
/**
- * If observability UI content loads with in 500ms
- * do not show skeleton.
+ * If content is not loaded within CONTENT_WAIT_MS,
+ * show the skeleton
*/
- clearTimeout(this.timerId);
- return;
- }
-
- /**
- * If observability UI content loads after 500ms
- * wait for 400ms to hide skeleton.
- * This is mostly to avoid the flashing effect If content loads imediately after skeleton
- */
- setTimeout(this.hideSkeleton, 400);
+ if (this.state !== SKELETON_STATE.HIDDEN) {
+ this.showSkeleton();
+ }
+ }, DEFAULT_TIMERS.CONTENT_WAIT_MS);
+ },
+ setErrorTimeout() {
+ this.errorTimeout = setTimeout(() => {
+ /**
+ * If content is not loaded within TIMEOUT_MS,
+ * show the error dialog
+ */
+ if (this.state !== SKELETON_STATE.HIDDEN) {
+ this.showError();
+ }
+ }, DEFAULT_TIMERS.TIMEOUT_MS);
},
hideSkeleton() {
- this.loading = false;
+ this.state = SKELETON_STATE.HIDDEN;
},
showSkeleton() {
- this.loading = true;
+ this.state = SKELETON_STATE.VISIBLE;
+ },
+ showError() {
+ this.state = SKELETON_STATE.ERROR;
+ },
+
+ isSkeletonShown(route) {
+ return this.variant === SKELETON_VARIANTS_BY_ROUTE[route];
},
},
};
</script>
<template>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
- <div v-show="loading" class="gl-px-5">
- <dashboards-skeleton v-if="variant === $options.SKELETON_VARIANT.DASHBOARDS" />
- <explore-skeleton v-else-if="variant === $options.SKELETON_VARIANT.EXPLORE" />
- <manage-skeleton v-else-if="variant === $options.SKELETON_VARIANT.MANAGE" />
+ <transition name="fade">
+ <div v-if="state === $options.SKELETON_STATE.VISIBLE" class="gl-px-5">
+ <dashboards-skeleton v-if="isSkeletonShown($options.OBSERVABILITY_ROUTES.DASHBOARDS)" />
+ <explore-skeleton v-else-if="isSkeletonShown($options.OBSERVABILITY_ROUTES.EXPLORE)" />
+ <manage-skeleton v-else-if="isSkeletonShown($options.OBSERVABILITY_ROUTES.MANAGE)" />
- <gl-skeleton-loader v-else>
- <rect y="2" width="10" height="8" />
- <rect y="2" x="15" width="15" height="8" />
- <rect y="2" x="35" width="15" height="8" />
- <rect y="15" width="400" height="30" />
- </gl-skeleton-loader>
- </div>
+ <gl-skeleton-loader v-else>
+ <rect y="2" width="10" height="8" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="15" height="8" />
+ <rect y="15" width="400" height="30" />
+ </gl-skeleton-loader>
+ </div>
+ </transition>
+
+ <gl-alert
+ v-if="state === $options.SKELETON_STATE.ERROR"
+ :title="$options.i18n.TIMEOUT_ERROR_LABEL"
+ variant="danger"
+ :dismissible="false"
+ class="gl-m-5"
+ >
+ {{ $options.i18n.TIMEOUT_ERROR_MESSAGE }}
+ </gl-alert>
<div
- v-show="!loading"
+ v-show="state === $options.SKELETON_STATE.HIDDEN"
+ data-testid="observability-wrapper"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
>
<slot></slot>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
index 74dd543e285..e4827dd169f 100644
--- a/app/assets/javascripts/observability/constants.js
+++ b/app/assets/javascripts/observability/constants.js
@@ -1,16 +1,32 @@
+import { __ } from '~/locale';
+
export const MESSAGE_EVENT_TYPE = Object.freeze({
GOUI_LOADED: 'GOUI_LOADED',
GOUI_ROUTE_UPDATE: 'GOUI_ROUTE_UPDATE',
});
export const OBSERVABILITY_ROUTES = Object.freeze({
- DASHBOARDS: '/groups/gitlab-org/-/observability/dashboards',
- EXPLORE: '/groups/gitlab-org/-/observability/explore',
- MANAGE: '/groups/gitlab-org/-/observability/manage',
+ DASHBOARDS: 'observability/dashboards',
+ EXPLORE: 'observability/explore',
+ MANAGE: 'observability/manage',
+});
+
+export const SKELETON_VARIANTS_BY_ROUTE = Object.freeze({
+ [OBSERVABILITY_ROUTES.DASHBOARDS]: 'dashboards',
+ [OBSERVABILITY_ROUTES.EXPLORE]: 'explore',
+ [OBSERVABILITY_ROUTES.MANAGE]: 'manage',
});
-export const SKELETON_VARIANT = Object.freeze({
- DASHBOARDS: 'dashboards',
- EXPLORE: 'explore',
- MANAGE: 'manage',
+export const SKELETON_STATE = Object.freeze({
+ ERROR: 'error',
+ VISIBLE: 'visible',
+ HIDDEN: 'hidden',
});
+
+export const DEFAULT_TIMERS = Object.freeze({
+ TIMEOUT_MS: 20000,
+ CONTENT_WAIT_MS: 500,
+});
+
+export const TIMEOUT_ERROR_LABEL = __('Unable to load the page');
+export const TIMEOUT_ERROR_MESSAGE = __('Reload the page to try again.');
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
index acf810257e6..38b601ac3ec 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
@@ -95,7 +95,7 @@ export default {
return formatDate(this.tag.createdAt, 'isoDate');
},
publishedTime() {
- return formatDate(this.tag.createdAt, 'hh:MM Z');
+ return formatDate(this.tag.createdAt, 'HH:MM:ss Z');
},
formattedRevision() {
// to be removed when API response is adjusted
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 23d8e97dd79..4f89d217623 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
@@ -12,7 +12,6 @@ import {
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
IMAGE_DELETE_SCHEDULED_STATUS,
- IMAGE_FAILED_DELETED_STATUS,
IMAGE_MIGRATING_STATE,
COPY_IMAGE_PATH_TITLE,
IMAGE_FULL_PATH_LABEL,
@@ -79,9 +78,6 @@ export default {
migrating() {
return this.item.migrationState === IMAGE_MIGRATING_STATE;
},
- failedDelete() {
- return this.item.status === IMAGE_FAILED_DELETED_STATUS;
- },
tagsCountText() {
return n__(
'ContainerRegistry|%{count} Tag',
@@ -99,9 +95,6 @@ export default {
}
return projectPath;
},
- routerLinkEvent() {
- return this.deleting ? '' : 'click';
- },
deleteButtonTooltipTitle() {
return this.migrating
? LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION
@@ -121,15 +114,7 @@ export default {
</script>
<template>
- <list-item
- v-gl-tooltip="{
- placement: 'left',
- disabled: !deleting,
- title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
- }"
- v-bind="$attrs"
- :disabled="deleting"
- >
+ <list-item v-bind="$attrs">
<template #left-primary>
<gl-button
v-if="!showFullPath"
@@ -143,12 +128,13 @@ export default {
:aria-label="$options.i18n.IMAGE_FULL_PATH_LABEL"
@click="hideButton"
/>
+ <span v-if="deleting" class="gl-text-gray-500">{{ imageName }}</span>
<router-link
+ v-else
ref="imageName"
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
data-qa-selector="registry_image_content"
- :event="routerLinkEvent"
:to="{ name: 'details', params: { id } }"
>
{{ imageName }}
@@ -163,21 +149,24 @@ export default {
</template>
<template #left-secondary>
<template v-if="!metadataLoading">
- <span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
- <gl-icon name="tag" class="gl-mr-2" />
- <gl-sprintf :message="tagsCountText">
- <template #count>
- {{ item.tagsCount }}
- </template>
- </gl-sprintf>
- </span>
+ <span v-if="deleting">{{ $options.i18n.ROW_SCHEDULED_FOR_DELETION }}</span>
+ <template v-else>
+ <span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
+ <gl-icon name="tag" class="gl-mr-2" />
+ <gl-sprintf :message="tagsCountText">
+ <template #count>
+ {{ item.tagsCount }}
+ </template>
+ </gl-sprintf>
+ </span>
- <cleanup-status
- v-if="item.expirationPolicyCleanupStatus"
- class="ml-2"
- :status="item.expirationPolicyCleanupStatus"
- :expiration-policy="expirationPolicy"
- />
+ <cleanup-status
+ v-if="item.expirationPolicyCleanupStatus"
+ class="gl-ml-2"
+ :status="item.expirationPolicyCleanupStatus"
+ :expiration-policy="expirationPolicy"
+ />
+ </template>
</template>
<div v-else class="gl-w-full">
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
index 020d78ad364..f2aa4916f48 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
@@ -53,7 +53,6 @@ export const TRACKING_ACTION_CLICK_SHOW_FULL_PATH = 'click_show_full_path';
// Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
-export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const IMAGE_MIGRATING_STATE = 'importing';
export const GRAPHQL_PAGE_SIZE = 10;
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 9b062024d03..850dca07a3f 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
@@ -20,7 +20,7 @@ export const apolloProvider = new VueApollo({
ContainerRepositoryDetails: {
fields: {
tags: {
- keyArgs: ['id'],
+ keyArgs: ['id', 'name', 'sort'],
merge: mergeVariables,
},
},
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue
index e5be98b87f7..06e4c38a179 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue
@@ -96,9 +96,9 @@ export default {
<history-item icon="commit" data-testid="first-pipeline-commit">
<gl-sprintf :message="$options.i18n.createdByCommitText">
<template #link>
- <gl-link :href="firstPipeline.project.commit_url"
- >#{{ truncate(firstPipeline.sha) }}</gl-link
- >
+ <gl-link :href="firstPipeline.project.commit_url">{{
+ truncate(firstPipeline.sha)
+ }}</gl-link>
</template>
<template #branch>
<strong>{{ firstPipeline.ref }}</strong>
@@ -147,7 +147,7 @@ export default {
>
<gl-sprintf :message="$options.i18n.combinedUpdateText">
<template #link>
- <gl-link :href="pipeline.project.commit_url">#{{ truncate(pipeline.sha) }}</gl-link>
+ <gl-link :href="pipeline.project.commit_url">{{ truncate(pipeline.sha) }}</gl-link>
</template>
<template #branch>
<strong>{{ pipeline.ref }}</strong>
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
index 2a1de2ae4a7..011a2668a8b 100644
--- 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
@@ -55,6 +55,7 @@ export default {
:action-cancel="$options.modal.cancelAction"
:title="$options.i18n.DELETE_PACKAGES_MODAL_TITLE"
@primary="$emit('confirm')"
+ @cancel="$emit('cancel')"
>
<span>{{ description }}</span>
</gl-modal>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
index a1fc7563de1..663c361819e 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
@@ -159,9 +159,9 @@ export default {
<history-item icon="commit" data-testid="first-pipeline-commit">
<gl-sprintf :message="$options.i18n.createdByCommitText">
<template #link>
- <gl-link :href="firstPipeline.commitPath" @click="trackCommitClick"
- >#{{ truncate(firstPipeline.sha) }}</gl-link
- >
+ <gl-link :href="firstPipeline.commitPath" @click="trackCommitClick">{{
+ truncate(firstPipeline.sha)
+ }}</gl-link>
</template>
<template #branch>
<strong>{{ firstPipeline.ref }}</strong>
@@ -212,9 +212,9 @@ export default {
>
<gl-sprintf :message="$options.i18n.combinedUpdateText">
<template #link>
- <gl-link :href="pipeline.commitPath" @click="trackCommitClick"
- >#{{ truncate(pipeline.sha) }}</gl-link
- >
+ <gl-link :href="pipeline.commitPath" @click="trackCommitClick">{{
+ truncate(pipeline.sha)
+ }}</gl-link>
</template>
<template #branch>
<strong>{{ pipeline.ref }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
index efc60c9c037..787f21d9419 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -1,13 +1,13 @@
<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';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
export default {
components: {
VersionRow,
- GlKeysetPagination,
PackagesListLoader,
+ RegistryList,
},
props: {
versions: {
@@ -26,9 +26,6 @@ export default {
},
},
computed: {
- showPagination() {
- return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
- },
isListEmpty() {
return this.versions.length === 0;
},
@@ -42,16 +39,18 @@ export default {
</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>
+ <registry-list
+ :hidden-delete="true"
+ :is-loading="isLoading"
+ :items="versions"
+ :pagination="pageInfo"
+ @prev-page="$emit('prev-page')"
+ @next-page="$emit('next-page')"
+ >
+ <template #default="{ item }">
+ <version-row :package-entity="item" />
+ </template>
+ </registry-list>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
index dd58f28a262..fdc6e75c932 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
@@ -30,7 +30,7 @@ export default {
computed: {
pypiPipCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
- return `pip install ${this.packageEntity.name} --extra-index-url ${this.packageEntity.pypiUrl}`;
+ return `pip install ${this.packageEntity.name} --index-url ${this.packageEntity.pypiUrl}`;
},
pypiSetupCommand() {
return `[gitlab]
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 ddcddf80c15..40bf7b7e143 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
@@ -5,10 +5,14 @@ import DeletePackageModal from '~/packages_and_registries/shared/components/dele
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 DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
+ DELETE_PACKAGES_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGES_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGES_TRACKING_ACTION,
PACKAGE_ERROR_STATUS,
} from '~/packages_and_registries/package_registry/constants';
import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils';
@@ -18,6 +22,7 @@ export default {
name: 'PackagesList',
components: {
GlAlert,
+ DeleteModal,
DeletePackageModal,
PackagesListLoader,
PackagesListRow,
@@ -44,6 +49,7 @@ export default {
data() {
return {
itemToBeDeleted: null,
+ itemsToBeDeleted: [],
errorPackages: [],
};
},
@@ -92,7 +98,18 @@ export default {
this.setItemToBeDeleted(item);
return;
}
- this.$emit('delete', items);
+ this.itemsToBeDeleted = items;
+ this.track(REQUEST_DELETE_PACKAGES_TRACKING_ACTION);
+ this.$refs.deletePackagesModal.show();
+ },
+ deleteItemsConfirmation() {
+ this.$emit('delete', this.itemsToBeDeleted);
+ this.track(DELETE_PACKAGES_TRACKING_ACTION);
+ this.itemsToBeDeleted = [];
+ },
+ deleteItemsCanceled() {
+ this.track(CANCEL_DELETE_PACKAGES_TRACKING_ACTION);
+ this.itemsToBeDeleted = [];
},
deleteItemConfirmation() {
this.$emit('package:delete', this.itemToBeDeleted);
@@ -159,6 +176,13 @@ export default {
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
/>
+
+ <delete-modal
+ ref="deletePackagesModal"
+ :items-to-be-deleted="itemsToBeDeleted"
+ @confirm="deleteItemsConfirmation"
+ @cancel="deleteItemsCanceled"
+ />
</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 b731cd77e66..539b12bd6db 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,11 @@ 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_TRACKING_ACTION = 'delete_packages';
+export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages';
+export const CANCEL_DELETE_PACKAGES_TRACKING_ACTION = 'cancel_delete_packages';
+
export const DELETE_PACKAGES_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting packages.',
);
@@ -184,9 +189,6 @@ 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/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 8b5d51cb856..396429d60d8 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,20 +1,18 @@
<script>
-import { GlAlert, GlBanner, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import { createAlert, VARIANT_INFO } from '~/flash';
-import { getCookie, historyReplaceState, parseBoolean, setCookie } from '~/lib/utils/common_utils';
+import { GlAlert, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { createAlert, VARIANT_INFO, VARIANT_SUCCESS, VARIANT_DANGER } from '~/flash';
+import { historyReplaceState } 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';
@@ -22,31 +20,26 @@ import DeletePackage from '~/packages_and_registries/package_registry/components
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: {
@@ -121,15 +114,13 @@ export default {
historyReplaceState(cleanUrl);
}
},
- async confirmDelete() {
- const { itemsToBeDeleted } = this;
- this.itemsToBeDeleted = [];
+ async deletePackages(packageEntities) {
this.mutationLoading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: destroyPackagesMutation,
variables: {
- ids: itemsToBeDeleted.map((i) => i.id),
+ ids: packageEntities.map((i) => i.id),
},
awaitRefetchQueries: true,
refetchQueries: [
@@ -144,30 +135,22 @@ export default {
throw new Error(data.destroyPackages.errors[0]);
}
this.showAlert({
- variant: 'success',
+ variant: VARIANT_SUCCESS,
message: DELETE_PACKAGES_SUCCESS_MESSAGE,
});
} catch {
this.showAlert({
- variant: 'danger',
+ variant: 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;
},
@@ -208,17 +191,11 @@ 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>
@@ -233,17 +210,6 @@ export default {
>
{{ 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 class="gl-mb-5" @update="handleSearchUpdate" />
@@ -261,7 +227,7 @@ export default {
@prev-page="fetchPreviousPage"
@next-page="fetchNextPage"
@package:delete="deletePackage"
- @delete="showDeletePackagesModal"
+ @delete="deletePackages"
>
<template #empty-state>
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
@@ -280,11 +246,5 @@ 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/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
index cc345fda7e8..d07d0a7673f 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue
@@ -87,13 +87,15 @@ export default {
<template>
<div>
- <div class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center">
- <gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2 gl-pt-2">
+ <div
+ v-if="!hiddenDelete"
+ class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center"
+ >
+ <gl-form-checkbox v-model="selectAll" class="gl-ml-2 gl-pt-2">
<span class="gl-font-weight-bold">{{ title }}</span>
</gl-form-checkbox>
<gl-button
- v-if="!hiddenDelete"
:disabled="disableDeleteButton"
category="secondary"
variant="danger"
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index a6e3a7dc08a..f1e92cf195a 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -1,4 +1,4 @@
-import initVariableList from '~/ci_variable_list';
+import initVariableList from '~/ci/ci_variable_list';
import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
import selfMonitor from '~/self_monitor';
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
deleted file mode 100644
index 40348e0b18a..00000000000
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import $ from 'jquery';
-import { debounce } from 'lodash';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { __ } from '~/locale';
-
-export default () => {
- const $broadcastMessageTheme = $('.js-broadcast-message-theme');
- const $broadcastMessageType = $('.js-broadcast-message-type');
- const $broadcastBannerMessagePreview = $('.js-broadcast-banner-message-preview [role="alert"]');
- const $broadcastMessage = $('.js-broadcast-message-message');
- const $jsBroadcastMessagePreview = $('#broadcast-message-preview');
-
- const reloadPreview = function reloadPreview() {
- const previewPath = $broadcastMessage.data('previewPath');
- const message = $broadcastMessage.val();
- const type = $broadcastMessageType.val();
- const theme = $broadcastMessageTheme.val();
-
- axios
- .post(previewPath, {
- broadcast_message: {
- message,
- broadcast_type: type,
- theme,
- },
- })
- .then(({ data }) => {
- $jsBroadcastMessagePreview.html(data);
- })
- .catch(() =>
- createAlert({
- message: __('An error occurred while rendering preview broadcast message'),
- }),
- );
- };
-
- $broadcastMessageTheme.on('change', reloadPreview);
-
- $broadcastMessageType.on('change', () => {
- const $broadcastMessageColorFormGroup = $('.js-broadcast-message-background-color-form-group');
- const $broadcastMessageDismissableFormGroup = $('.js-broadcast-message-dismissable-form-group');
- const $broadcastNotificationMessagePreview = $('.js-broadcast-notification-message-preview');
-
- $broadcastMessageColorFormGroup.toggleClass('hidden');
- $broadcastMessageDismissableFormGroup.toggleClass('hidden');
- $broadcastBannerMessagePreview.toggleClass('hidden');
- $broadcastNotificationMessagePreview.toggleClass('hidden');
-
- reloadPreview();
- });
-
- $broadcastMessage.on(
- 'input',
- debounce(() => {
- reloadPreview();
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- );
-};
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
index 25036984082..94cae500a1e 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
@@ -1,8 +1,3 @@
import initEditBroadcastMessage from '~/admin/broadcast_messages/edit';
-import initBroadcastMessagesForm from '../broadcast_message';
-if (gon.features.vueBroadcastMessages) {
- initEditBroadcastMessage();
-} else {
- initBroadcastMessagesForm();
-}
+initEditBroadcastMessage();
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
index 1f37df2b340..2662496be05 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
@@ -1,10 +1,3 @@
import initBroadcastMessages from '~/admin/broadcast_messages';
-import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-import initBroadcastMessagesForm from '../broadcast_message';
-if (gon.features.vueBroadcastMessages) {
- initBroadcastMessages();
-} else {
- initBroadcastMessagesForm();
- initDeprecatedRemoveRowBehavior();
-}
+initBroadcastMessages();
diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js
index bde0007ec6a..23f5b083589 100644
--- a/app/assets/javascripts/pages/groups/boards/index.js
+++ b/app/assets/javascripts/pages/groups/boards/index.js
@@ -1,7 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
-import UsersSelect from '~/users_select';
-new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initBoards();
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 58ca195d7b9..fb685247bd4 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -2,7 +2,7 @@ import { GROUP_BADGE } from '~/badges/constants';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import initTransferGroupForm from '~/groups/init_transfer_group_form';
-import groupsSelect from '~/groups_select';
+import { initGroupSelects } from '~/vue_shared/components/group_select/init_group_selects';
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select';
@@ -20,7 +20,7 @@ dirtySubmitFactory(
mountBadgeSettings(GROUP_BADGE);
// Initialize Subgroups selector
-groupsSelect();
+initGroupSelects();
projectSelect();
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index 62d47cb49b8..ceda2c8fa17 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -11,7 +11,7 @@ import { groupLinkRequestFormatter } from '~/members/utils';
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
const APP_OPTIONS = {
[MEMBER_TYPES.user]: {
- tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
+ tableFields: SHARED_FIELDS.concat(['source', 'activity']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: [
'account',
diff --git a/app/assets/javascripts/pages/groups/observability/datasources/index.js b/app/assets/javascripts/pages/groups/observability/datasources/index.js
new file mode 100644
index 00000000000..c3b6ce6f99f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/observability/datasources/index.js
@@ -0,0 +1,3 @@
+import ObservabilityApp from '~/observability';
+
+ObservabilityApp();
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index b1a1cc21764..184958bd189 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -1,5 +1,5 @@
import initStaleRunnerCleanupSetting from 'ee_else_ce/group_settings/stale_runner_cleanup';
-import initVariableList from '~/ci_variable_list';
+import initVariableList from '~/ci/ci_variable_list';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import initSettingsPanels from '~/settings_panels';
import initDeployTokens from '~/deploy_tokens';
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index 161fca83a58..53bceb3a6f0 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -1,7 +1,9 @@
import leaveByUrl from '~/namespaces/leave_by_url';
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
+import initReadMore from '~/read_more';
import initGroupDetails from '../shared/group_details';
leaveByUrl('group');
initGroupDetails();
initGroupOverviewTabs();
+initReadMore();
diff --git a/app/assets/javascripts/pages/import/bitbucket/status/index.js b/app/assets/javascripts/pages/import/bitbucket/status/index.js
index 6e9c26bf930..0b0f222ab76 100644
--- a/app/assets/javascripts/pages/import/bitbucket/status/index.js
+++ b/app/assets/javascripts/pages/import/bitbucket/status/index.js
@@ -1,21 +1,6 @@
-import Vue from 'vue';
-import { initStoreFromElement, initPropsFromElement } from '~/import_entities/import_projects';
+import mountImportProjectsTable from '~/import_entities/import_projects';
import BitbucketStatusTable from '~/import_entities/import_projects/components/bitbucket_status_table.vue';
-function importBitBucket() {
- const mountElement = document.getElementById('import-projects-mount-element');
- if (!mountElement) return undefined;
+const mountElement = document.getElementById('import-projects-mount-element');
- const store = initStoreFromElement(mountElement);
- const attrs = initPropsFromElement(mountElement);
-
- return new Vue({
- el: mountElement,
- store,
- render(createElement) {
- return createElement(BitbucketStatusTable, { attrs });
- },
- });
-}
-
-importBitBucket();
+mountImportProjectsTable({ mountElement, Component: BitbucketStatusTable });
diff --git a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
index 90eb423c7a7..680ff0ddcde 100644
--- a/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
+++ b/app/assets/javascripts/pages/import/bitbucket_server/status/index.js
@@ -1,24 +1,10 @@
-import Vue from 'vue';
-import { initStoreFromElement, initPropsFromElement } from '~/import_entities/import_projects';
-import BitbucketServerStatusTable from './components/bitbucket_server_status_table.vue';
-
-function BitbucketServerStatus() {
- const mountElement = document.getElementById('import-projects-mount-element');
- if (!mountElement) return undefined;
+import mountImportProjectsTable from '~/import_entities/import_projects';
- const store = initStoreFromElement(mountElement);
- const attrs = initPropsFromElement(mountElement);
- const { reconfigurePath } = mountElement.dataset;
-
- return new Vue({
- el: mountElement,
- store,
- render(createElement) {
- return createElement(BitbucketServerStatusTable, {
- attrs: { ...attrs, reconfigurePath },
- });
- },
- });
-}
+import BitbucketServerStatusTable from './components/bitbucket_server_status_table.vue';
-BitbucketServerStatus();
+const mountElement = document.getElementById('import-projects-mount-element');
+mountImportProjectsTable({
+ mountElement,
+ Component: BitbucketServerStatusTable,
+ extraProps: ({ reconfigurePath }) => ({ reconfigurePath }),
+});
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index 6feb4c2188f..3dcababb4fd 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -1,5 +1,13 @@
<script>
-import { GlButton, GlEmptyState, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import {
+ GlButton,
+ GlEmptyState,
+ GlIcon,
+ GlLink,
+ GlLoadingIcon,
+ GlTableLite,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import { createAlert } from '~/flash';
@@ -34,15 +42,20 @@ export default {
components: {
GlButton,
GlEmptyState,
+ GlIcon,
GlLink,
GlLoadingIcon,
- GlTable,
+ GlTableLite,
PaginationBar,
ImportStatus,
TimeAgo,
LocalStorageSync,
},
+ directives: {
+ GlTooltip,
+ },
+
data() {
return {
loading: true,
@@ -58,12 +71,12 @@ export default {
fields: [
tableCell({
key: 'source_full_path',
- label: s__('BulkImport|Source group'),
+ label: s__('BulkImport|Source'),
thClass: `${DEFAULT_TH_CLASSES} gl-w-30p`,
}),
tableCell({
key: 'destination_name',
- label: s__('BulkImport|Destination group'),
+ label: s__('BulkImport|Destination'),
thClass: `${DEFAULT_TH_CLASSES} gl-w-40p`,
}),
tableCell({
@@ -113,12 +126,24 @@ export default {
}
},
- getDestinationUrl({ destination_name: name, destination_namespace: namespace }) {
- return [namespace, name].filter(Boolean).join('/');
+ getFullDestinationUrl(params) {
+ return joinPaths(gon.relative_url_root || '', '/', params.destination_full_path);
},
- getFullDestinationUrl(params) {
- return joinPaths(gon.relative_url_root || '', '/', this.getDestinationUrl(params));
+ getPresentationUrl(item) {
+ const suffix = item.entity_type === 'group' ? '/' : '';
+ return `${item.destination_full_path}${suffix}`;
+ },
+
+ getEntityTooltip(item) {
+ switch (item.entity_type) {
+ case 'project':
+ return __('Project');
+ case 'group':
+ return __('Group');
+ default:
+ return '';
+ }
},
},
@@ -134,26 +159,36 @@ export default {
>
<h1 class="gl-my-0 gl-py-4 gl-font-size-h1">
<img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
- {{ s__('BulkImport|Group import history') }}
+ {{ s__('BulkImport|GitLab Migration history') }}
</h1>
</div>
<gl-loading-icon v-if="loading" size="lg" class="gl-mt-5" />
<gl-empty-state
v-else-if="!hasHistoryItems"
:title="s__('BulkImport|No history is available')"
- :description="s__('BulkImport|Your imported groups will appear here.')"
+ :description="s__('BulkImport|Your imported groups and projects will appear here.')"
/>
<template v-else>
- <gl-table
+ <gl-table-lite
:fields="$options.fields"
:items="historyItems"
data-qa-selector="import_history_table"
class="gl-w-full"
>
<template #cell(destination_name)="{ item }">
- <gl-link :href="getFullDestinationUrl(item)" target="_blank">
- {{ getDestinationUrl(item) }}
- </gl-link>
+ <template v-if="item.destination_full_path">
+ <gl-icon
+ v-gl-tooltip
+ :name="item.entity_type"
+ :title="getEntityTooltip(item)"
+ :aria-label="getEntityTooltip(item)"
+ class="gl-text-gray-500"
+ />
+ <gl-link :href="getFullDestinationUrl(item)" target="_blank">
+ {{ getPresentationUrl(item) }}
+ </gl-link>
+ </template>
+ <gl-loading-icon v-else inline />
</template>
<template #cell(created_at)="{ value }">
<time-ago :time="value" />
@@ -171,7 +206,7 @@ export default {
<template #row-details="{ item }">
<pre><code>{{ item.failures }}</code></pre>
</template>
- </gl-table>
+ </gl-table-lite>
<pagination-bar
:page-info="pageInfo"
class="gl-m-0 gl-mt-3"
diff --git a/app/assets/javascripts/pages/import/fogbugz/status/index.js b/app/assets/javascripts/pages/import/fogbugz/status/index.js
index 4c427b72372..30ee468734d 100644
--- a/app/assets/javascripts/pages/import/fogbugz/status/index.js
+++ b/app/assets/javascripts/pages/import/fogbugz/status/index.js
@@ -2,4 +2,4 @@ import mountImportProjectsTable from '~/import_entities/import_projects';
const mountElement = document.getElementById('import-projects-mount-element');
-mountImportProjectsTable(mountElement);
+mountImportProjectsTable({ mountElement });
diff --git a/app/assets/javascripts/pages/import/gitea/status/index.js b/app/assets/javascripts/pages/import/gitea/status/index.js
index 4c427b72372..30ee468734d 100644
--- a/app/assets/javascripts/pages/import/gitea/status/index.js
+++ b/app/assets/javascripts/pages/import/gitea/status/index.js
@@ -2,4 +2,4 @@ import mountImportProjectsTable from '~/import_entities/import_projects';
const mountElement = document.getElementById('import-projects-mount-element');
-mountImportProjectsTable(mountElement);
+mountImportProjectsTable({ mountElement });
diff --git a/app/assets/javascripts/pages/import/github/status/index.js b/app/assets/javascripts/pages/import/github/status/index.js
index 4c427b72372..30ee468734d 100644
--- a/app/assets/javascripts/pages/import/github/status/index.js
+++ b/app/assets/javascripts/pages/import/github/status/index.js
@@ -2,4 +2,4 @@ import mountImportProjectsTable from '~/import_entities/import_projects';
const mountElement = document.getElementById('import-projects-mount-element');
-mountImportProjectsTable(mountElement);
+mountImportProjectsTable({ mountElement });
diff --git a/app/assets/javascripts/pages/import/gitlab/status/index.js b/app/assets/javascripts/pages/import/gitlab/status/index.js
index 4c427b72372..30ee468734d 100644
--- a/app/assets/javascripts/pages/import/gitlab/status/index.js
+++ b/app/assets/javascripts/pages/import/gitlab/status/index.js
@@ -2,4 +2,4 @@ import mountImportProjectsTable from '~/import_entities/import_projects';
const mountElement = document.getElementById('import-projects-mount-element');
-mountImportProjectsTable(mountElement);
+mountImportProjectsTable({ mountElement });
diff --git a/app/assets/javascripts/pages/import/manifest/status/index.js b/app/assets/javascripts/pages/import/manifest/status/index.js
index 4c427b72372..30ee468734d 100644
--- a/app/assets/javascripts/pages/import/manifest/status/index.js
+++ b/app/assets/javascripts/pages/import/manifest/status/index.js
@@ -2,4 +2,4 @@ import mountImportProjectsTable from '~/import_entities/import_projects';
const mountElement = document.getElementById('import-projects-mount-element');
-mountImportProjectsTable(mountElement);
+mountImportProjectsTable({ mountElement });
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index af0097b415c..46704d96552 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -78,3 +78,5 @@ if (filesContainer.length) {
loadAwardsHandler();
initCommitActions();
+
+syntaxHighlight([document.querySelector('.files')]);
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index b74f7d1cf57..760bf3f7131 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -2,6 +2,7 @@ import Diff from '~/diff';
import GpgBadges from '~/gpg_badges';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import initCompareSelector from '~/projects/compare';
+import syntaxHighlight from '~/syntax_highlight';
initCompareSelector();
@@ -9,3 +10,5 @@ new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
+
+syntaxHighlight([document.querySelector('.files')]);
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 91650003d4a..2028af8b8f0 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -57,6 +57,9 @@ export default {
visibilityHelpPath: {
default: '',
},
+ cancelPath: {
+ default: '',
+ },
projectFullPath: {
default: '',
},
@@ -97,7 +100,7 @@ export default {
required: false,
skipValidation: true,
}),
- visibility: initFormField({ value: this.getInitialVisibilityValue() }),
+ visibility: initFormField({ value: null }),
},
};
return {
@@ -106,8 +109,39 @@ export default {
};
},
computed: {
+ projectVisibilityLevel() {
+ return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility];
+ },
+ namespaceVisibilityLevel() {
+ const visibility =
+ this.form.fields.namespace.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING;
+ return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
+ },
+ visibilityLevelCap() {
+ return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel);
+ },
+ restrictedVisibilityLevelsSet() {
+ return new Set(this.restrictedVisibilityLevels);
+ },
allowedVisibilityLevels() {
- return this.getAllowedVisibilityLevels();
+ const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce(
+ (levels, [levelName, levelValue]) => {
+ if (
+ !this.restrictedVisibilityLevelsSet.has(levelValue) &&
+ levelValue <= this.visibilityLevelCap
+ ) {
+ levels.push(levelName);
+ }
+ return levels;
+ },
+ [],
+ );
+
+ if (!allowedLevels.length) {
+ return [VISIBILITY_LEVEL_PRIVATE_STRING];
+ }
+
+ return allowedLevels;
},
visibilityLevels() {
return [
@@ -143,13 +177,15 @@ export default {
this.form.fields.slug.value = kebabCase(newVal);
},
},
+ created() {
+ this.form.fields.visibility.value = this.getMaximumAllowedVisibilityLevel(
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+ );
+ },
methods: {
isVisibilityLevelDisabled(visibility) {
return !this.allowedVisibilityLevels.includes(visibility);
},
- getInitialVisibilityValue() {
- return this.getMaximumAllowedVisibilityLevel(this.projectVisibility);
- },
setNamespace(namespace) {
this.form.fields.namespace.value = namespace;
this.form.fields.namespace.state = true;
@@ -157,42 +193,8 @@ export default {
this.form.fields.visibility.value,
);
},
- getProjectVisibilityLevel() {
- return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility];
- },
- getNamespaceVisibilityLevel() {
- const visibility =
- this.form?.fields?.namespace?.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING;
- return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
- },
- getVisibilityLevelCap() {
- return Math.min(this.getProjectVisibilityLevel(), this.getNamespaceVisibilityLevel());
- },
- getRestrictedVisibilityLevelsSet() {
- return new Set(this.restrictedVisibilityLevels);
- },
- getAllowedVisibilityLevels() {
- const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce(
- (levels, [levelName, levelValue]) => {
- if (
- !this.getRestrictedVisibilityLevelsSet().has(levelValue) &&
- levelValue <= this.getVisibilityLevelCap()
- ) {
- levels.push(levelName);
- }
- return levels;
- },
- [],
- );
-
- if (!allowedLevels.length) {
- return [VISIBILITY_LEVEL_PRIVATE_STRING];
- }
-
- return allowedLevels;
- },
getMaximumAllowedVisibilityLevel(visibility) {
- const allowedVisibilities = this.getAllowedVisibilityLevels().map(
+ const allowedVisibilities = this.allowedVisibilityLevels.map(
(s) => VISIBILITY_LEVELS_STRING_TO_INTEGER[s],
);
const current = VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
@@ -373,7 +375,7 @@ export default {
class="gl-mr-3"
data-testid="cancel-button"
:disabled="isSaving"
- :href="projectFullPath"
+ :href="cancelPath"
>
{{ s__('ForkProject|Cancel') }}
</gl-button>
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
index 00e0649deed..5e0c5735bc0 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
@@ -1,14 +1,5 @@
<script>
-import {
- GlButton,
- GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlTruncate,
-} from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlCollapsibleListbox } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { s__ } from '~/locale';
@@ -20,12 +11,7 @@ export default {
components: {
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlTruncate,
+ GlCollapsibleListbox,
},
apollo: {
project: {
@@ -61,24 +47,25 @@ export default {
};
},
computed: {
+ loading() {
+ return this.$apollo.queries.project.loading;
+ },
rootUrl() {
return `${gon.gitlab_url}/`;
},
namespaces() {
return this.project.forkTargets?.nodes || [];
},
- hasMatches() {
- return this.namespaces.length;
- },
dropdownText() {
return this.selectedNamespace?.fullPath || s__('ForkProject|Select a namespace');
},
+ namespaceItems() {
+ return this.namespaces?.map(({ id, fullPath }) => ({ value: id, text: fullPath }));
+ },
},
methods: {
- handleDropdownShown() {
- this.$refs.search.focusInput();
- },
- setNamespace(namespace) {
+ setNamespace(namespaceId) {
+ const namespace = this.namespaces.find(({ id }) => id === namespaceId);
const id = getIdFromGraphQLId(namespace.id);
this.$emit('select', {
@@ -89,6 +76,9 @@ export default {
this.selectedNamespace = { id, fullPath: namespace.fullPath };
},
+ searchNamespaces(search) {
+ this.search = search;
+ },
},
};
</script>
@@ -98,39 +88,19 @@ export default {
<gl-button class="gl-text-truncate gl-flex-grow-0! gl-max-w-34" label :title="rootUrl">{{
rootUrl
}}</gl-button>
-
- <gl-dropdown
+ <gl-collapsible-listbox
class="gl-flex-grow-1"
- toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20"
data-qa-selector="select_namespace_dropdown"
data-testid="select_namespace_dropdown"
- no-flip
- @shown="handleDropdownShown"
- >
- <template #button-text>
- <gl-truncate :text="dropdownText" position="start" with-tooltip />
- </template>
- <gl-search-box-by-type
- ref="search"
- v-model.trim="search"
- :is-loading="$apollo.queries.project.loading"
- data-qa-selector="select_namespace_dropdown_search_field"
- data-testid="select_namespace_dropdown_search_field"
- />
- <template v-if="!$apollo.queries.project.loading">
- <template v-if="hasMatches">
- <gl-dropdown-section-header>{{ __('Namespaces') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="namespace of namespaces"
- :key="namespace.id"
- data-qa-selector="select_namespace_dropdown_item"
- @click="setNamespace(namespace)"
- >
- {{ namespace.fullPath }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-text v-else>{{ __('No matches found') }}</gl-dropdown-text>
- </template>
- </gl-dropdown>
+ :items="namespaceItems"
+ :header-text="__('Namespaces')"
+ :no-results-text="__('No matches found')"
+ :searchable="true"
+ :searching="loading"
+ toggle-class="gl-flex-direction-column gl-align-items-stretch!"
+ :toggle-text="dropdownText"
+ @search="searchNamespaces"
+ @select="setNamespace"
+ />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js
index d3a5ce5390f..a31b8b1a1f4 100644
--- a/app/assets/javascripts/pages/projects/forks/new/index.js
+++ b/app/assets/javascripts/pages/projects/forks/new/index.js
@@ -11,6 +11,7 @@ const {
newGroupPath,
projectFullPath,
visibilityHelpPath,
+ cancelPath,
projectId,
projectName,
projectPath,
@@ -30,6 +31,7 @@ new Vue({
provide: {
newGroupPath,
visibilityHelpPath,
+ cancelPath,
endpoint,
projectFullPath,
projectId,
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 7380055cbbf..37cf345fe77 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,9 +1,7 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initTerraformNotification from '~/projects/terraform_notification';
-import { initSidebarTracking } from '../shared/nav/sidebar_tracking';
import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
-initSidebarTracking();
initTerraformNotification();
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 3717d8027c4..d9b0dbbb9b0 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
@@ -22,7 +22,7 @@ export default {
GlTooltip,
},
i18n: {
- contactAdmin: s__('LearnGitlab|Contact your administrator to start a free Ultimate trial.'),
+ contactAdmin: s__('LearnGitlab|Contact your administrator to enable this action.'),
viewAdminList: s__('LearnGitlab|View administrator list'),
watchHow: __('Watch how'),
},
@@ -50,6 +50,9 @@ export default {
openInNewTab() {
return ACTION_LABELS[this.action]?.openInNewTab === true || this.value.openInNewTab === true;
},
+ popoverText() {
+ return this.value.message || this.$options.i18n.contactAdmin;
+ },
},
methods: {
openModal() {
@@ -101,7 +104,7 @@ export default {
category="tertiary"
icon="question-o"
class="ml-auto"
- :aria-label="$options.i18n.contactAdmin"
+ :aria-label="popoverText"
size="small"
data-testid="contact-admin-popover-trigger"
/>
@@ -111,7 +114,7 @@ export default {
triggers="hover focus"
data-testid="contact-admin-popover"
>
- <p>{{ $options.i18n.contactAdmin }}</p>
+ <p>{{ popoverText }}</p>
<gl-link
:href="value.url"
class="font-size-inherit"
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 a4e3ddfc506..d4734b8842d 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
@@ -4,14 +4,12 @@ import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import Diff from '~/diff';
import GLForm from '~/gl_form';
import LabelsSelect from '~/labels/labels_select';
import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
export default () => {
- new Diff();
new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 568bf19b55e..f0a955e5360 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,5 +1,7 @@
import initNotesApp from '~/mr_notes/init_notes';
+import { initReportAbuse } from '~/projects/merge_requests';
import { initMrPage } from '../page';
initMrPage();
initNotesApp();
+initReportAbuse();
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
index 97e436920c7..6947b15dcbe 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const initShowExperiment = () => {
const element = document.querySelector('#js-show-ml-experiment');
@@ -13,6 +14,7 @@ const initShowExperiment = () => {
const candidates = JSON.parse(element.dataset.candidates);
const metricNames = JSON.parse(element.dataset.metrics);
const paramNames = JSON.parse(element.dataset.params);
+ const pagination = convertObjectPropsToCamelCase(JSON.parse(element.dataset.pagination));
// eslint-disable-next-line no-new
new Vue({
@@ -21,6 +23,7 @@ const initShowExperiment = () => {
candidates,
metricNames,
paramNames,
+ pagination,
},
render(h) {
return h(MlExperiment);
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 27610df482d..4bdbb70d942 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -67,10 +67,9 @@ function initTakeownershipModal() {
});
}
-initPipelineSchedulesCallout();
-
if (gon.features?.pipelineSchedulesVue) {
initPipelineSchedulesApp();
} else {
+ initPipelineSchedulesCallout();
initTakeownershipModal();
}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index fd8b1a6290f..242c5a1a97b 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -54,7 +54,7 @@ export default {
inputNameAttribute: 'schedule[cron]',
radioValue: this.initialCronInterval ? KEY_CUSTOM : KEY_EVERY_DAY,
cronInterval: this.initialCronInterval,
- cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron',
+ cronSyntaxUrl: 'https://docs.gitlab.com/ee/topics/cron/',
};
},
computed: {
@@ -95,7 +95,7 @@ export default {
},
{
value: KEY_CUSTOM,
- text: s__('PipelineScheduleIntervalPattern|Custom (%{linkStart}Cron syntax%{linkEnd})'),
+ text: s__('PipelineScheduleIntervalPattern|Custom (%{linkStart}Learn more.%{linkEnd})'),
link: this.cronSyntaxUrl,
},
];
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 abd17efc498..8440d0e77cd 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
@@ -3,7 +3,7 @@ import Vue from 'vue';
import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
-import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
+import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
import GlFieldErrors from '~/gl_field_errors';
import Translate from '~/vue_shared/translate';
import { initTimezoneDropdown } from '../../../profiles/init_timezone_dropdown';
diff --git a/app/assets/javascripts/pages/projects/pipelines/new/index.js b/app/assets/javascripts/pages/projects/pipelines/new/index.js
index e1f71965853..9b94b8ba96b 100644
--- a/app/assets/javascripts/pages/projects/pipelines/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/new/index.js
@@ -1,3 +1,3 @@
-import initNewPipelineForm from '~/pipeline_new/index';
+import initNewPipelineForm from '~/ci/pipeline_new/index';
initNewPipelineForm();
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 9a7fd74fd8c..2fd372a45b8 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -20,7 +20,7 @@ initImportProjectMembersTrigger();
const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list-app'), {
[MEMBER_TYPES.user]: {
- tableFields: SHARED_FIELDS.concat(['source', 'granted', 'userCreatedAt', 'lastActivityOn']),
+ tableFields: SHARED_FIELDS.concat(['source', 'activity']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: [
'account',
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index 8909ff1f221..895c7d0a18e 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -1,7 +1,7 @@
import initArtifactsSettings from '~/artifacts_settings';
import SecretValues from '~/behaviors/secret_values';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
-import initVariableList from '~/ci_variable_list';
+import initVariableList from '~/ci/ci_variable_list';
import initDeployFreeze from '~/deploy_freeze';
import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments';
@@ -11,6 +11,7 @@ import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
import { initCiSecureFiles } from '~/ci_secure_files';
import initDeployTokens from '~/deploy_tokens';
+import { initProjectRunners } from '~/ci/runner/project_runners';
// Initialize expandable settings panels
initSettingsPanels();
@@ -37,11 +38,13 @@ document.querySelector('.js-toggle-extra-settings').addEventListener('click', (e
registrySettingsApp();
initDeployTokens();
initDeployFreeze();
-
initSettingsPipelinesTriggers();
initArtifactsSettings();
+
+initProjectRunners();
initSharedRunnersToggle();
initInstallRunner();
initRunnerAwsDeployments();
+
initTokenAccess();
initCiSecureFiles();
diff --git a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
index 0f7ede8ed42..40741be5f53 100644
--- a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
+++ b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
@@ -1,7 +1,4 @@
-import groupsSelect from '~/groups_select';
import UserCallout from '~/user_callout';
-groupsSelect();
-
// eslint-disable-next-line no-new
new UserCallout({ className: 'js-mr-approval-callout' });
diff --git a/app/assets/javascripts/pages/projects/usage_quotas/index.js b/app/assets/javascripts/pages/projects/usage_quotas/index.js
new file mode 100644
index 00000000000..885b8ca8e12
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/usage_quotas/index.js
@@ -0,0 +1,9 @@
+import initProjectStorage from '~/usage_quotas/storage/init_project_storage';
+import initSearchSettings from '~/search_settings';
+
+const initVueApp = () => {
+ initProjectStorage('js-project-storage-count-app');
+};
+
+initVueApp();
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index 5cbb7a06bc1..30c351359e4 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
+import { initReportAbuse } from '~/users/profile';
import UserTabs from './user_tabs';
function initUserProfile(action) {
@@ -19,3 +20,4 @@ const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
+initReportAbuse();
diff --git a/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue b/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
deleted file mode 100644
index d35d2010150..00000000000
--- a/app/assets/javascripts/pipeline_new/components/refs_dropdown.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
-import { debounce } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
-import { BRANCH_REF_TYPE, TAG_REF_TYPE, DEBOUNCE_REFS_SEARCH_MS } from '../constants';
-import formatRefs from '../utils/format_refs';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- },
- inject: ['projectRefsEndpoint'],
- props: {
- value: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- },
- data() {
- return {
- isLoading: false,
- searchTerm: '',
- branches: [],
- tags: [],
- };
- },
- computed: {
- lowerCasedSearchTerm() {
- return this.searchTerm.toLowerCase();
- },
- refShortName() {
- return this.value.shortName;
- },
- hasTags() {
- return this.tags.length > 0;
- },
- },
- watch: {
- searchTerm() {
- this.debouncedLoadRefs();
- },
- },
- methods: {
- loadRefs() {
- this.isLoading = true;
-
- axios
- .get(this.projectRefsEndpoint, {
- params: {
- search: this.lowerCasedSearchTerm,
- },
- })
- .then(({ data }) => {
- // Note: These keys are uppercase in API
- const { Branches = [], Tags = [] } = data;
-
- this.branches = formatRefs(Branches, BRANCH_REF_TYPE);
- this.tags = formatRefs(Tags, TAG_REF_TYPE);
- })
- .catch((e) => {
- this.$emit('loadingError', e);
- })
- .finally(() => {
- this.isLoading = false;
- });
- },
- debouncedLoadRefs: debounce(function debouncedLoadRefs() {
- this.loadRefs();
- }, DEBOUNCE_REFS_SEARCH_MS),
- setRefSelected(ref) {
- this.$emit('input', ref);
- },
- isSelected(ref) {
- return ref.fullName === this.value.fullName;
- },
- },
-};
-</script>
-<template>
- <gl-dropdown :text="refShortName" block data-testid="ref-select" @show.once="loadRefs">
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- :is-loading="isLoading"
- :placeholder="__('Search refs')"
- data-testid="search-refs"
- />
- <gl-dropdown-section-header>{{ __('Branches') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="branch in branches"
- :key="branch.fullName"
- class="gl-font-monospace"
- is-check-item
- :is-checked="isSelected(branch)"
- @click="setRefSelected(branch)"
- >
- {{ branch.shortName }}
- </gl-dropdown-item>
- <gl-dropdown-section-header v-if="hasTags">{{ __('Tags') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="tag in tags"
- :key="tag.fullName"
- class="gl-font-monospace"
- is-check-item
- :is-checked="isSelected(tag)"
- @click="setRefSelected(tag)"
- >
- {{ tag.shortName }}
- </gl-dropdown-item>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/pipeline_new/utils/format_refs.js b/app/assets/javascripts/pipeline_new/utils/format_refs.js
deleted file mode 100644
index f0fbc5ed7b6..00000000000
--- a/app/assets/javascripts/pipeline_new/utils/format_refs.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '../constants';
-
-export default (refs, type) => {
- let fullName;
-
- return refs.map((ref) => {
- if (type === BRANCH_REF_TYPE) {
- fullName = `refs/heads/${ref}`;
- } else if (type === TAG_REF_TYPE) {
- fullName = `refs/tags/${ref}`;
- }
-
- return {
- shortName: ref,
- fullName,
- };
- });
-};
diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
index adeb4ae598b..ab837d04d9a 100644
--- a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue
@@ -6,6 +6,7 @@ import { merge } from '~/lib/utils/yaml';
import { __ } from '~/locale';
import { isValidStepSeq } from '~/pipeline_wizard/validators';
import Tracking from '~/tracking';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import YamlEditor from './editor.vue';
import WizardStep from './step.vue';
import CommitStep from './commit.vue';
@@ -28,7 +29,7 @@ export default {
WizardStep,
CommitStep,
},
- mixins: [trackingMixin],
+ mixins: [trackingMixin, glFeatureFlagsMixin()],
props: {
steps: {
type: Object,
@@ -91,6 +92,11 @@ export default {
category: `pipeline_wizard:${this.templateId}`,
};
},
+ trackingExtraData() {
+ return {
+ features: this.glFeatures,
+ };
+ },
},
watch: {
isLastStep(value) {
@@ -125,6 +131,7 @@ export default {
extra: {
fromStep: this.currentStepIndex + 1,
toStep: this.currentStepIndex,
+ ...this.trackingExtraData,
},
});
},
@@ -136,6 +143,7 @@ export default {
extra: {
fromStep: this.currentStepIndex - 1,
toStep: this.currentStepIndex,
+ ...this.trackingExtraData,
},
});
},
@@ -144,6 +152,7 @@ export default {
this.track('click_button', {
label: 'pipeline_wizard_commit',
property: 'commit',
+ extra: this.trackingExtraData,
});
},
onEditorTouched() {
@@ -151,6 +160,7 @@ export default {
label: 'pipeline_wizard_editor_interaction',
extra: {
currentStep: this.currentStepIndex,
+ ...this.trackingExtraData,
},
});
},
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
index c56537f4039..041b62e02ec 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__ } from '~/locale';
import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
@@ -12,7 +12,7 @@ export default {
fields: DEFAULT_FIELDS,
retry: __('Retry'),
components: {
- CiBadge,
+ CiBadgeLink,
GlButton,
GlLink,
GlTableLite,
@@ -72,7 +72,7 @@ export default {
<div
class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
>
- <ci-badge :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
+ <ci-badge-link :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
<div class="gl-text-truncate">
<gl-link
:href="item.detailedStatus.detailsPath"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 30528ce8d17..c498f12d5c7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
import { isEqual } from 'lodash';
import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/flash';
import { getParameterByName } from '~/lib/utils/url_utility';
@@ -26,8 +26,7 @@ export default {
PipelineKeyOptions,
components: {
EmptyState,
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlEmptyState,
GlIcon,
GlLoadingIcon,
@@ -315,7 +314,7 @@ export default {
this.updateContent(this.requestData);
},
changeVisibilityPipelineID(val) {
- this.selectedPipelineKeyOption = val;
+ this.selectedPipelineKeyOption = PipelineKeyOptions.find((e) => val === e.value);
},
},
};
@@ -355,21 +354,12 @@ export default {
:params="validatedParams"
@filterPipelines="filterPipelines"
/>
- <gl-dropdown
- class="gl-display-flex"
- :text="selectedPipelineKeyOption.text"
- data-testid="pipeline-key-dropdown"
- >
- <gl-dropdown-item
- v-for="(val, index) in $options.PipelineKeyOptions"
- :key="index"
- :is-checked="selectedPipelineKeyOption.key === val.key"
- is-check-item
- @click="changeVisibilityPipelineID(val)"
- >
- {{ val.text }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-collapsible-listbox
+ data-testid="pipeline-key-collapsible-box"
+ :toggle-text="selectedPipelineKeyOption.text"
+ :items="$options.PipelineKeyOptions"
+ @select="changeVisibilityPipelineID"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
index 936ae4da1ec..00ab8a25ca1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
@@ -1,12 +1,12 @@
<script>
import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
PipelinesTimeago,
},
mixins: [Tracking.mixin()],
@@ -38,14 +38,13 @@ export default {
<template>
<div>
- <ci-badge
+ <ci-badge-link
class="gl-mb-3"
:status="pipelineStatus"
:show-text="!isChildView"
- :icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
@ciStatusBadgeClick="trackClick"
/>
- <pipelines-timeago class="gl-mt-3" :pipeline="pipeline" />
+ <pipelines-timeago :pipeline="pipeline" />
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 346f5735576..ed32d643c0e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -161,7 +161,7 @@ export default {
<pipeline-url
:pipeline="item"
:pipeline-schedule-url="pipelineScheduleUrl"
- :pipeline-key="pipelineKeyOption.key"
+ :pipeline-key="pipelineKeyOption.value"
/>
</template>
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 cd44c998074..960af030421 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
@@ -19,53 +19,51 @@ export default {
duration() {
return this.pipeline?.details?.duration;
},
- finishedTime() {
- return this.pipeline?.details?.finished_at;
- },
- skipped() {
- return this.pipeline?.details?.status?.label === 'skipped';
- },
- stuck() {
- return this.pipeline.flags.stuck;
- },
durationFormatted() {
return durationTimeFormatted(this.duration);
},
+ finishedTime() {
+ return this.pipeline?.details?.finished_at;
+ },
showInProgress() {
return !this.duration && !this.finishedTime && !this.skipped;
},
showSkipped() {
return !this.duration && !this.finishedTime && this.skipped;
},
+ skipped() {
+ return this.pipeline?.details?.status?.label === 'skipped';
+ },
+ stuck() {
+ return this.pipeline.flags.stuck;
+ },
},
};
</script>
<template>
- <div class="gl-display-block">
- <span v-if="showInProgress" data-testid="pipeline-in-progress">
+ <div class="gl-display-flex gl-flex-direction-column time-ago">
+ <span
+ v-if="showInProgress"
+ class="gl-display-inline-flex gl-align-items-center"
+ data-testid="pipeline-in-progress"
+ >
<gl-icon v-if="stuck" name="warning" class="gl-mr-2" :size="12" data-testid="warning-icon" />
- <gl-icon
- v-else
- name="hourglass"
- class="gl-vertical-align-baseline! gl-mr-2"
- :size="12"
- data-testid="hourglass-icon"
- />
+ <gl-icon v-else name="hourglass" class="gl-mr-2" :size="12" data-testid="hourglass-icon" />
{{ s__('Pipeline|In progress') }}
</span>
<span v-if="showSkipped" data-testid="pipeline-skipped">
- <gl-icon name="status_skipped_borderless" class="gl-mr-2" :size="16" />
+ <gl-icon name="status_skipped_borderless" />
{{ s__('Pipeline|Skipped') }}
</span>
- <p v-if="duration" class="duration">
- <gl-icon name="timer" class="gl-vertical-align-baseline!" :size="12" />
+ <p v-if="duration" class="duration gl-display-inline-flex gl-align-items-center">
+ <gl-icon name="timer" class="gl-mr-2" :size="12" />
{{ durationFormatted }}
</p>
- <p v-if="finishedTime" class="finished-at d-none d-md-block">
- <gl-icon name="calendar" class="gl-vertical-align-baseline!" :size="12" />
+ <p v-if="finishedTime" class="finished-at gl-display-inline-flex gl-align-items-center">
+ <gl-icon name="calendar" class="gl-mr-2" :size="12" />
<time
v-gl-tooltip
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index ed8ec614304..2f37f90e625 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -71,12 +71,12 @@ export const PipelineKeyOptions = [
{
text: __('Show Pipeline ID'),
label: __('Pipeline ID'),
- key: 'id',
+ value: 'id',
},
{
text: __('Show Pipeline IID'),
label: __('Pipeline IID'),
- key: 'iid',
+ value: 'iid',
},
];
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 07551c2342f..e6770b71113 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -2,7 +2,7 @@ import Visibility from 'visibilityjs';
import { createAlert } from '~/flash';
import { helpPagePath } from '~/helpers/help_page_helper';
import { historyPushState, buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import { validateParams } from '~/pipelines/utils';
@@ -196,7 +196,7 @@ export default {
this.updateTable();
})
.catch((e) => {
- const unauthorized = e.response.status === httpStatusCodes.UNAUTHORIZED;
+ const unauthorized = e.response.status === HTTP_STATUS_UNAUTHORIZED;
let errorMessage = __(
'An error occurred while trying to run a new pipeline for this merge request.',
);
diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
index 6520e68d41c..8e4d42a42c6 100644
--- a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
+++ b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
+import { initListboxInputs } from '~/vue_shared/components/listbox_input/init_listbox_inputs';
import ProfilePreferences from './components/profile_preferences.vue';
export default () => {
+ initListboxInputs();
+
const el = document.querySelector('#js-profile-preferences-app');
const formEl = document.querySelector('#profile-preferences-form');
const shouldParse = ['integrationViews', 'themes', 'userFields'];
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 93bc203d391..c031c5e5e8e 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -30,10 +30,6 @@ export default class Profile {
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('.js-group-notification-email').on('change', this.submitForm);
- $('#user_notification_email').on('select2-selecting', (event) => {
- setTimeout(this.submitForm.bind(event.currentTarget));
- });
$('#user_email_opted_in').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
this.form.on('submit', this.onSubmitForm);
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index a037e721677..a1fc3f1a731 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlSearchBoxByType,
- GlDropdownItem,
- GlDropdownText,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
@@ -16,11 +10,7 @@ import {
export default {
name: 'BranchesDropdown',
components: {
- GlDropdown,
- GlSearchBoxByType,
- GlDropdownItem,
- GlDropdownText,
- GlLoadingIcon,
+ GlCollapsibleListbox,
},
props: {
value: {
@@ -46,13 +36,16 @@ export default {
},
computed: {
...mapGetters(['joinedBranches']),
- ...mapState(['isFetching', 'branch', 'branches']),
+ ...mapState(['isFetching']),
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.joinedBranches.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
},
+ listboxItems() {
+ return this.filteredResults.map((value) => ({ value, text: value }));
+ },
},
watch: {
// Parent component can set the branch value (e.g. when the user selects a different project)
@@ -68,10 +61,6 @@ export default {
...mapActions(['fetchBranches']),
selectBranch(branch) {
this.$emit('selectBranch', branch);
- this.searchTerm = branch; // enables isSelected to work as expected
- },
- isSelected(selectedBranch) {
- return selectedBranch === this.branch;
},
searchTermChanged(value) {
this.searchTerm = value;
@@ -81,36 +70,16 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="value" :header-text="$options.i18n.branchHeaderTitle">
- <gl-search-box-by-type
- :value="searchTerm"
- trim
- autocomplete="off"
- :debounce="250"
- :placeholder="$options.i18n.branchSearchPlaceholder"
- data-testid="dropdown-search-box"
- @input="searchTermChanged"
- />
- <gl-dropdown-item
- v-for="branch in filteredResults"
- v-show="!isFetching"
- :key="branch"
- :name="branch"
- :is-checked="isSelected(branch)"
- is-check-item
- data-testid="dropdown-item"
- @click="selectBranch(branch)"
- >
- {{ branch }}
- </gl-dropdown-item>
- <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
- <gl-loading-icon size="sm" class="gl-mx-auto" />
- </gl-dropdown-text>
- <gl-dropdown-text
- v-if="!filteredResults.length && !isFetching"
- data-testid="empty-result-message"
- >
- <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
- </gl-dropdown-text>
- </gl-dropdown>
+ <gl-collapsible-listbox
+ :header-text="$options.i18n.branchHeaderTitle"
+ :toggle-text="value"
+ :items="listboxItems"
+ searchable
+ :search-placeholder="$options.i18n.branchSearchPlaceholder"
+ :searching="isFetching"
+ :selected="value"
+ :no-results-text="$options.i18n.noResultsMessage"
+ @search="searchTermChanged"
+ @select="selectBranch"
+ />
</template>
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index 1febe8ceaab..b31ba4a100c 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -141,11 +141,7 @@ export default {
:value="targetProjectId"
/>
- <projects-dropdown
- class="gl-w-half"
- :value="targetProjectName"
- @selectProject="setSelectedProject"
- />
+ <projects-dropdown :value="targetProjectName" @selectProject="setSelectedProject" />
</gl-form-group>
<gl-form-group
@@ -155,12 +151,7 @@ export default {
>
<input id="start_branch" type="hidden" name="start_branch" :value="branch" />
- <branches-dropdown
- class="gl-w-half"
- :value="branch"
- :blanked="isRevert"
- @selectBranch="setBranch"
- />
+ <branches-dropdown :value="branch" :blanked="isRevert" @selectBranch="setBranch" />
</gl-form-group>
<gl-form-checkbox
diff --git a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
index 6288bcdaad0..d43f5b99e2c 100644
--- a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlDropdownText } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
import {
I18N_NO_RESULTS_MESSAGE,
@@ -10,10 +10,7 @@ import {
export default {
name: 'ProjectsDropdown',
components: {
- GlDropdown,
- GlSearchBoxByType,
- GlDropdownItem,
- GlDropdownText,
+ GlCollapsibleListbox,
},
props: {
value: {
@@ -41,17 +38,20 @@ export default {
project.name.toLowerCase().includes(lowerCasedFilterTerm),
);
},
+ listboxItems() {
+ return this.filteredResults.map(({ id, name }) => ({ value: id, text: name }));
+ },
selectedProject() {
return this.sortedProjects.find((project) => project.id === this.targetProjectId) || {};
},
},
methods: {
- selectProject(project) {
- this.$emit('selectProject', project.id);
- this.filterTerm = project.name; // when we select a project, we want the dropdown to filter to the selected project
- },
- isSelected(selectedProject) {
- return selectedProject === this.selectedProject;
+ selectProject(value) {
+ this.$emit('selectProject', value);
+
+ // when we select a project, we want the dropdown to filter to the selected project
+ const project = this.listboxItems.find((x) => x.value === value);
+ this.filterTerm = project?.text || '';
},
filterTermChanged(value) {
this.filterTerm = value;
@@ -60,28 +60,15 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="selectedProject.name" :header-text="$options.i18n.projectHeaderTitle">
- <gl-search-box-by-type
- :value="filterTerm"
- trim
- autocomplete="off"
- :placeholder="$options.i18n.projectSearchPlaceholder"
- data-testid="dropdown-search-box"
- @input="filterTermChanged"
- />
- <gl-dropdown-item
- v-for="project in filteredResults"
- :key="project.name"
- :name="project.name"
- :is-checked="isSelected(project)"
- is-check-item
- data-testid="dropdown-item"
- @click="selectProject(project)"
- >
- {{ project.name }}
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredResults.length" data-testid="empty-result-message">
- <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span>
- </gl-dropdown-text>
- </gl-dropdown>
+ <gl-collapsible-listbox
+ :header-text="$options.i18n.projectHeaderTitle"
+ :items="listboxItems"
+ searchable
+ :search-placeholder="$options.i18n.projectSearchPlaceholder"
+ :selected="selectedProject.id"
+ :toggle-text="selectedProject.name"
+ :no-results-text="$options.i18n.noResultsMessage"
+ @search="filterTermChanged"
+ @select="selectProject"
+ />
</template>
diff --git a/app/assets/javascripts/projects/commits/index.js b/app/assets/javascripts/projects/commits/index.js
index 53169f689c9..f56884f605f 100644
--- a/app/assets/javascripts/projects/commits/index.js
+++ b/app/assets/javascripts/projects/commits/index.js
@@ -33,20 +33,31 @@ export const initCommitsRefSwitcher = () => {
if (!el) return false;
- const { projectId, ref, commitsPath } = el.dataset;
+ const { projectId, ref, commitsPath, refType } = el.dataset;
const commitsPathPrefix = commitsPath.match(COMMITS_PATH_REGEX)?.[0];
-
+ const useSymbolicRefNames = Boolean(refType);
return new Vue({
el,
render(createElement) {
return createElement(RefSelector, {
props: {
projectId,
- value: ref,
+ value: useSymbolicRefNames ? `refs/${refType}/${ref}` : ref,
+ useSymbolicRefNames,
+ refType,
},
on: {
input(selected) {
- visitUrl(`${commitsPathPrefix}/${selected}`);
+ if (useSymbolicRefNames) {
+ const matches = selected.match(/refs\/(heads|tags)\/(.+)/);
+ if (matches) {
+ visitUrl(`${commitsPathPrefix}/${matches[2]}?ref_type=${matches[1]}`);
+ } else {
+ visitUrl(`${commitsPathPrefix}/${selected}`);
+ }
+ } else {
+ visitUrl(`${commitsPathPrefix}/${selected}`);
+ }
},
},
});
diff --git a/app/assets/javascripts/projects/merge_requests/components/report_abuse_dropdown_item.vue b/app/assets/javascripts/projects/merge_requests/components/report_abuse_dropdown_item.vue
new file mode 100644
index 00000000000..31890249f41
--- /dev/null
+++ b/app/assets/javascripts/projects/merge_requests/components/report_abuse_dropdown_item.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { MountingPortal } from 'portal-vue';
+import { s__ } from '~/locale';
+
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+export default {
+ name: 'ReportAbuseDropdownItem',
+ components: {
+ GlDropdownItem,
+ MountingPortal,
+ AbuseCategorySelector,
+ },
+ i18n: {
+ reportAbuse: s__('ReportAbuse|Report abuse to administrator'),
+ },
+ data() {
+ return {
+ open: false,
+ };
+ },
+ methods: {
+ openDrawer() {
+ this.open = true;
+ },
+ closeDrawer() {
+ this.open = false;
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ <gl-dropdown-item @click="openDrawer">{{ $options.i18n.reportAbuse }}</gl-dropdown-item>
+
+ <mounting-portal mount-to="#js-report-abuse-drawer" name="abuse-category-selector" append>
+ <abuse-category-selector :show-drawer="open" @close-drawer="closeDrawer" />
+ </mounting-portal>
+ </span>
+</template>
diff --git a/app/assets/javascripts/projects/merge_requests/index.js b/app/assets/javascripts/projects/merge_requests/index.js
new file mode 100644
index 00000000000..25a70121d68
--- /dev/null
+++ b/app/assets/javascripts/projects/merge_requests/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import ReportAbuseDropdownItem from './components/report_abuse_dropdown_item.vue';
+
+export const initReportAbuse = () => {
+ const el = document.getElementById('js-report-abuse-dropdown-item');
+
+ if (!el) return false;
+
+ const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: { reportAbusePath, reportedUserId, reportedFromUrl },
+ render(createElement) {
+ return createElement(ReportAbuseDropdownItem);
+ },
+ });
+};
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 626ed67c466..6260c8dd4d0 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
@@ -56,6 +56,7 @@ export default {
},
update({ project: { branchRules } }) {
const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch);
+ this.branchRule = branchRule;
this.branchProtection = branchRule?.branchProtection;
this.approvalRules = branchRule?.approvalRules;
this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
@@ -69,6 +70,7 @@ export default {
branchProtection: {},
approvalRules: {},
statusChecks: [],
+ branchRule: {},
matchingBranchesCount: null,
};
},
@@ -88,12 +90,12 @@ export default {
},
allowedToMergeHeader() {
return sprintf(this.$options.i18n.allowedToMergeHeader, {
- total: this.mergeAccessLevels.total,
+ total: this.mergeAccessLevels?.total || 0,
});
},
allowedToPushHeader() {
return sprintf(this.$options.i18n.allowedToPushHeader, {
- total: this.pushAccessLevels.total,
+ total: this.pushAccessLevels?.total || 0,
});
},
approvalsHeader() {
@@ -141,7 +143,7 @@ export default {
<template>
<gl-loading-icon v-if="$apollo.loading" />
- <div v-else-if="!branchProtection">{{ $options.i18n.noData }}</div>
+ <div v-else-if="!branchRule">{{ $options.i18n.noData }}</div>
<div v-else>
<strong data-testid="branch-title">{{ branchTitle }}</strong>
<p v-if="!allBranches" class="gl-mb-3 gl-text-gray-400">
diff --git a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
index 5ca864a412b..54120b3525d 100644
--- a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
+++ b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
@@ -4,6 +4,10 @@ import SharedRunnersToggle from '~/projects/settings/components/shared_runners_t
export default (containerId = 'toggle-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
+ if (!containerEl) {
+ return null;
+ }
+
const {
isDisabledAndUnoverridable,
isEnabled,
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 41947834bdb..4a24df4b0dc 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
@@ -5,6 +5,7 @@ import { getAccessLevels } from '../../../utils';
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'),
@@ -62,6 +63,9 @@ export default {
isWildcard() {
return this.name.includes('*');
},
+ isProtected() {
+ return Boolean(this.branchProtection);
+ },
hasApprovalDetails() {
return this.approvalDetails.length;
},
@@ -105,10 +109,10 @@ export default {
if (this.isWildcard) {
approvalDetails.push(this.matchingBranchesText);
}
- if (this.branchProtection.allowForcePush) {
+ if (this.branchProtection?.allowForcePush) {
approvalDetails.push(this.$options.i18n.allowForcePush);
}
- if (this.branchProtection.codeOwnerApprovalRequired) {
+ if (this.branchProtection?.codeOwnerApprovalRequired) {
approvalDetails.push(this.$options.i18n.codeOwnerApprovalRequired);
}
if (this.statusChecksTotal) {
@@ -154,6 +158,10 @@ 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>
diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js
index 769782607b8..692f375bb94 100644
--- a/app/assets/javascripts/read_more.js
+++ b/app/assets/javascripts/read_more.js
@@ -31,9 +31,9 @@ export default function initReadMore(triggerSelector = '.js-read-more-trigger')
triggerEl.addEventListener(
'click',
- (e) => {
+ () => {
targetEl.classList.add('is-expanded');
- e.target.remove();
+ triggerEl.remove();
},
{ once: true },
);
diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue
index 4fa2a92ff03..52d1ed96b21 100644
--- a/app/assets/javascripts/ref/components/ref_results_section.vue
+++ b/app/assets/javascripts/ref/components/ref_results_section.vue
@@ -74,6 +74,11 @@ export default {
required: false,
default: '',
},
+ shouldShowCheck: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
totalCountText() {
@@ -82,6 +87,9 @@ export default {
},
methods: {
showCheck(item) {
+ if (!this.shouldShowCheck) {
+ return false;
+ }
return item.name === this.selectedRef || item.value === this.selectedRef;
},
},
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index b75958e2ced..10967fb84ed 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -15,6 +15,8 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
+ BRANCH_REF_TYPE,
+ TAG_REF_TYPE,
} from '../constants';
import createStore from '../stores';
import RefResultsSection from './ref_results_section.vue';
@@ -50,6 +52,11 @@ export default {
required: false,
default: '',
},
+ refType: {
+ type: String,
+ required: false,
+ default: null,
+ },
projectId: {
type: String,
required: true,
@@ -146,6 +153,12 @@ export default {
buttonText() {
return this.selectedRefForDisplay || this.i18n.noRefSelected;
},
+ isTagRefType() {
+ return this.refType === TAG_REF_TYPE;
+ },
+ isBranchRefType() {
+ return this.refType === BRANCH_REF_TYPE;
+ },
},
watch: {
// Keep the Vuex store synchronized if the parent
@@ -273,6 +286,7 @@ export default {
:show-header="showSectionHeaders"
data-testid="branches-section"
data-qa-selector="branches_section"
+ :should-show-check="!useSymbolicRefNames || isBranchRefType"
@selected="selectRef($event)"
/>
@@ -289,6 +303,7 @@ export default {
:error-message="i18n.tagsErrorMessage"
:show-header="showSectionHeaders"
data-testid="tags-section"
+ :should-show-check="!useSymbolicRefNames || isTagRefType"
@selected="selectRef($event)"
/>
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index 397e3ed2ac8..f4faa535166 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -5,6 +5,8 @@ export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS';
export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]);
+export const BRANCH_REF_TYPE = 'heads';
+export const TAG_REF_TYPE = 'tags';
export const X_TOTAL_HEADER = 'x-total';
diff --git a/app/assets/javascripts/ref/stores/mutations.js b/app/assets/javascripts/ref/stores/mutations.js
index e078d3333d4..9846ac0adb7 100644
--- a/app/assets/javascripts/ref/stores/mutations.js
+++ b/app/assets/javascripts/ref/stores/mutations.js
@@ -1,5 +1,5 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { X_TOTAL_HEADER } from '../constants';
import * as types from './mutation_types';
@@ -86,7 +86,7 @@ export default {
// 404's are expected when the search query doesn't match any commits
// and shouldn't be treated as an actual error
- error: error.response?.status !== httpStatusCodes.NOT_FOUND ? error : null,
+ error: error.response?.status !== HTTP_STATUS_NOT_FOUND ? error : null,
};
},
[types.RESET_COMMIT_MATCHES](state) {
diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js
deleted file mode 100644
index c283fb1ea08..00000000000
--- a/app/assets/javascripts/ref_select_dropdown.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-
-class RefSelectDropdown {
- constructor($dropdownButton, availableRefs) {
- const availableRefsValue =
- availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML);
- initDeprecatedJQueryDropdown($dropdownButton, {
- data: availableRefsValue,
- filterable: true,
- filterByText: true,
- remote: false,
- fieldName: $dropdownButton.data('fieldName'),
- filterInput: 'input[type="search"]',
- selectable: true,
- isSelectable(branch, $el) {
- return !$el.hasClass('is-active');
- },
- text(branch) {
- return branch;
- },
- id(branch) {
- return branch;
- },
- toggleLabel(branch) {
- return branch;
- },
- });
-
- const $dropdownContainer = $dropdownButton.closest('.dropdown');
- const $fieldInput = $(`input[name="${$dropdownButton.data('fieldName')}"]`, $dropdownContainer);
- const $filterInput = $('input[type="search"]', $dropdownContainer);
-
- $filterInput.on('keyup', (e) => {
- const keyCode = e.keyCode || e.which;
- if (keyCode !== 13) return;
-
- const ref = $filterInput.val().trim();
- if (ref === '') {
- return;
- }
-
- $fieldInput.val(ref);
- $('.dropdown-toggle-text', $dropdownButton).text(ref);
-
- $dropdownContainer.removeClass('open');
- });
- }
-}
-
-export default RefSelectDropdown;
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index f009c0310c5..d029f8cf89f 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -35,7 +35,7 @@ const fetchData = (projectPath, path, ref, offset) => {
gon.relative_url_root || '/',
projectPath,
'/-/refs/',
- ref,
+ encodeURIComponent(ref),
'/logs_tree/',
encodeURIComponent(removeLeadingSlash(path)),
);
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index 3e6d2e675ed..a480710f8ac 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -10,6 +10,8 @@ const viewers = {
audio: () => import('./audio_viewer.vue'),
svg: () => import('./image_viewer.vue'),
sketch: () => import('./sketch_viewer.vue'),
+ notebook: () => import('./notebook_viewer.vue'),
+ openapi: () => import('./openapi_viewer.vue'),
};
export const loadViewer = (type, isUsingLfs) => {
diff --git a/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue
new file mode 100644
index 00000000000..1114a0942ec
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/notebook_viewer.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import notebookLoader from '~/blob/notebook';
+import { stripPathTail } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ url: this.blob.rawPath,
+ };
+ },
+ mounted() {
+ notebookLoader({ el: this.$refs.viewer, relativeRawPath: stripPathTail(this.url) });
+ },
+};
+</script>
+
+<template>
+ <div ref="viewer" :data-endpoint="url" data-testid="notebook">
+ <gl-loading-icon class="gl-my-4" size="lg" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/openapi_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/openapi_viewer.vue
new file mode 100644
index 00000000000..5665e4b0ec4
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/openapi_viewer.vue
@@ -0,0 +1,24 @@
+<script>
+import renderOpenApi from '~/blob/openapi';
+
+export default {
+ props: {
+ blob: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ url: this.blob.rawPath,
+ };
+ },
+ mounted() {
+ renderOpenApi(this.$refs.viewer);
+ },
+};
+</script>
+
+<template>
+ <div ref="viewer" class="file-content" :data-endpoint="url" data-testid="openapi"></div>
+</template>
diff --git a/app/assets/javascripts/repository/components/fork_info.vue b/app/assets/javascripts/repository/components/fork_info.vue
new file mode 100644
index 00000000000..980fa140eb5
--- /dev/null
+++ b/app/assets/javascripts/repository/components/fork_info.vue
@@ -0,0 +1,146 @@
+<script>
+import { GlIcon, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { s__, sprintf, n__ } from '~/locale';
+import { createAlert } from '~/flash';
+import forkDetailsQuery from '../queries/fork_details.query.graphql';
+
+export const i18n = {
+ forkedFrom: s__('ForkedFromProjectPath|Forked from'),
+ inaccessibleProject: s__('ForkedFromProjectPath|Forked from an inaccessible project.'),
+ upToDate: s__('ForksDivergence|Up to date with the upstream repository.'),
+ unknown: s__('ForksDivergence|This fork has diverged from the upstream repository.'),
+ behind: s__('ForksDivergence|%{behind} %{commit_word} behind'),
+ ahead: s__('ForksDivergence|%{ahead} %{commit_word} ahead of'),
+ behindAndAhead: s__('ForksDivergence|%{messages} the upstream repository.'),
+ error: s__('ForksDivergence|Failed to fetch fork details. Try again later.'),
+};
+
+export default {
+ i18n,
+ components: {
+ GlIcon,
+ GlLink,
+ GlSkeletonLoader,
+ },
+ apollo: {
+ project: {
+ query: forkDetailsQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ ref: this.selectedRef,
+ };
+ },
+ skip() {
+ return !this.sourceName;
+ },
+ error(error) {
+ createAlert({
+ message: this.$options.i18n.error,
+ captureError: true,
+ error,
+ });
+ },
+ },
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ selectedRef: {
+ type: String,
+ required: true,
+ },
+ sourceName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ sourcePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ project: {
+ forkDetails: {
+ ahead: null,
+ behind: null,
+ },
+ },
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.project.loading;
+ },
+ ahead() {
+ return this.project?.forkDetails?.ahead;
+ },
+ behind() {
+ return this.project?.forkDetails?.behind;
+ },
+ behindText() {
+ return sprintf(this.$options.i18n.behind, {
+ behind: this.behind,
+ commit_word: n__('commit', 'commits', this.behind),
+ });
+ },
+ aheadText() {
+ return sprintf(this.$options.i18n.ahead, {
+ ahead: this.ahead,
+ commit_word: n__('commit', 'commits', this.ahead),
+ });
+ },
+ isUnknownDivergence() {
+ return (!this.ahead && this.ahead !== 0) || (!this.behind && this.behind !== 0);
+ },
+ behindAheadMessage() {
+ const messages = [];
+ if (this.behind > 0) {
+ messages.push(this.behindText);
+ }
+ if (this.ahead > 0) {
+ messages.push(this.aheadText);
+ }
+ return messages.join(', ');
+ },
+ hasBehindAheadMessage() {
+ return this.behindAheadMessage.length > 0;
+ },
+ forkDivergenceMessage() {
+ if (this.isUnknownDivergence) {
+ return this.$options.i18n.unknown;
+ }
+ if (this.hasBehindAheadMessage) {
+ return sprintf(this.$options.i18n.behindAndAhead, {
+ messages: this.behindAheadMessage,
+ });
+ }
+ return this.$options.i18n.upToDate;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="info-well gl-sm-display-flex gl-flex-direction-column">
+ <div class="well-segment gl-p-5 gl-w-full gl-display-flex">
+ <gl-icon name="fork" :size="16" class="gl-display-block gl-m-4 gl-text-center" />
+ <div v-if="sourceName">
+ {{ $options.i18n.forkedFrom }}
+ <gl-link data-qa-selector="forked_from_link" :href="sourcePath">{{ sourceName }}</gl-link>
+ <gl-skeleton-loader v-if="isLoading" :lines="1" />
+ <div v-else class="gl-text-secondary">
+ {{ forkDivergenceMessage }}
+ </div>
+ </div>
+ <div v-else data-testid="inaccessible-project" class="gl-align-items-center gl-display-flex">
+ {{ $options.i18n.inaccessibleProject }}
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 4a8f83458f4..f6d6004ba96 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -2,12 +2,13 @@
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { createAlert } from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { __ } from '~/locale';
import {
TREE_PAGE_SIZE,
TREE_INITIAL_FETCH_COUNT,
TREE_PAGE_LIMIT,
COMMIT_BATCH_SIZE,
+ GITALY_UNAVAILABLE_CODE,
+ i18n,
} from '../constants';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
@@ -17,6 +18,7 @@ import FilePreview from './preview/index.vue';
import FileTable from './table/index.vue';
export default {
+ i18n,
components: {
FileTable,
FilePreview,
@@ -142,10 +144,19 @@ export default {
}
})
.catch((error) => {
+ let gitalyUnavailableError;
+ if (error.graphQLErrors) {
+ gitalyUnavailableError = error.graphQLErrors.find(
+ (e) => e?.extensions?.code === GITALY_UNAVAILABLE_CODE,
+ );
+ }
+ const message = gitalyUnavailableError
+ ? this.$options.i18n.gitalyError
+ : this.$options.i18n.generalError;
createAlert({
- message: __('An error occurred while fetching folder content.'),
+ message,
+ captureError: true,
});
- throw error;
});
},
normalizeData(key, data) {
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index e194bddcc56..5098053c4f7 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -1,5 +1,6 @@
import { __ } from '~/locale';
+export const GITALY_UNAVAILABLE_CODE = 'unavailable';
export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
@@ -100,3 +101,8 @@ export const LEGACY_FILE_TYPES = [
'cargo_toml',
'go_mod',
];
+
+export const i18n = {
+ generalError: __('An error occurred while fetching folder content.'),
+ gitalyError: __('Error: Gitaly is unavailable. Contact your administrator.'),
+};
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index e9214e3acff..e5d22f50d72 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -11,6 +11,7 @@ import RefSelector from '~/ref/components/ref_selector.vue';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
+import ForkInfo from './components/fork_info.vue';
import LastCommit from './components/last_commit.vue';
import BlobControls from './components/blob_controls.vue';
import apolloProvider from './graphql';
@@ -63,6 +64,28 @@ export default function setupVueRepositoryList() {
},
});
+ const initForkInfo = () => {
+ const forkEl = document.getElementById('js-fork-info');
+ if (!forkEl) {
+ return null;
+ }
+ const { sourceName, sourcePath } = forkEl.dataset;
+ return new Vue({
+ el: forkEl,
+ apolloProvider,
+ render(h) {
+ return h(ForkInfo, {
+ props: {
+ projectPath,
+ selectedRef: ref,
+ sourceName,
+ sourcePath,
+ },
+ });
+ },
+ });
+ };
+
const initLastCommitApp = () =>
new Vue({
el: document.getElementById('js-last-commit'),
@@ -118,6 +141,7 @@ export default function setupVueRepositoryList() {
initLastCommitApp();
initBlobControlsApp();
+ initForkInfo();
initRefSwitcher();
router.afterEach(({ params: { path } }) => {
diff --git a/app/assets/javascripts/repository/queries/fork_details.query.graphql b/app/assets/javascripts/repository/queries/fork_details.query.graphql
new file mode 100644
index 00000000000..d1a37d00d55
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/fork_details.query.graphql
@@ -0,0 +1,9 @@
+query getForkDetails($projectPath: ID!, $ref: String) {
+ project(fullPath: $projectPath) {
+ id
+ forkDetails(ref: $ref) {
+ ahead
+ behind
+ }
+ }
+}
diff --git a/app/assets/javascripts/repository/utils/ref_switcher_utils.js b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
index 8ff52104c93..f296b5e9b4a 100644
--- a/app/assets/javascripts/repository/utils/ref_switcher_utils.js
+++ b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
@@ -17,6 +17,7 @@ const NAMESPACE_TARGET_REGEX = /(\/-\/(blob|tree))\/.*?\/(.*)/;
*/
export function generateRefDestinationPath(projectRootPath, selectedRef) {
const currentPath = window.location.pathname;
+ const encodedHash = '%23';
let namespace = '/-/tree';
let target;
const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
@@ -24,7 +25,12 @@ export function generateRefDestinationPath(projectRootPath, selectedRef) {
[, namespace, , target] = match;
}
- const destinationPath = joinPaths(projectRootPath, namespace, selectedRef, target);
+ const destinationPath = joinPaths(
+ projectRootPath,
+ namespace,
+ encodeURI(selectedRef).replace(/#/g, encodedHash),
+ target,
+ );
return `${destinationPath}${window.location.hash}`;
}
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index ba12f31ef87..d4ee857c9c1 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,6 +1,7 @@
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { queryToObject } from '~/lib/utils/url_utility';
import refreshCounts from '~/pages/search/show/refresh_counts';
+import syntaxHighlight from '~/syntax_highlight';
import { initSidebar, sidebarInitState } from './sidebar';
import { initSearchSort } from './sort';
import createStore from './store';
@@ -8,10 +9,14 @@ import { initTopbar } from './topbar';
import { initBlobRefSwitcher } from './under_topbar';
export const initSearchApp = () => {
- const query = queryToObject(window.location.search);
- const navigation = sidebarInitState();
+ syntaxHighlight(document.querySelectorAll('.js-search-results'));
+ const query = queryToObject(window.location.search, { gatherArrays: true });
+ const { navigationJsonParsed: navigation } = sidebarInitState() || {};
- const store = createStore({ query, navigation });
+ const store = createStore({
+ query,
+ navigation,
+ });
initTopbar(store);
initSidebar(store);
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
index 4ddf695f61a..fbfc24a94ae 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
@@ -21,6 +21,6 @@ export default {
<template>
<div>
<radio-filter :class="ffBasedXPadding" :filter-data="$options.confidentialFilterData" />
- <hr class="gl-my-5 gl-border-gray-100" />
+ <hr class="gl-my-5 gl-mx-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue
index 9b993ab9a86..ff7a044736d 100644
--- a/app/assets/javascripts/search/sidebar/components/results_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue
@@ -44,7 +44,7 @@ export default {
<form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery">
<hr
v-if="searchPageVerticalNavFeatureFlag"
- class="gl-my-5 gl-border-gray-100 gl-display-none gl-md-display-block"
+ class="gl-my-5 gl-mx-5 gl-border-gray-100 gl-display-none gl-md-display-block"
/>
<status-filter v-if="showStatusFilter" />
<confidentiality-filter v-if="showConfidentialityFilter" />
diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
index 7a03306e2f9..3c280a5d696 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
@@ -1,13 +1,10 @@
<script>
import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import { formatNumber, s__ } from '~/locale';
+import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import {
- NAV_LINK_DEFAULT_CLASSES,
- NUMBER_FORMATING_OPTIONS,
- NAV_LINK_COUNT_DEFAULT_CLASSES,
-} from '../constants';
+import { NAV_LINK_DEFAULT_CLASSES, NAV_LINK_COUNT_DEFAULT_CLASSES } from '../constants';
+import { formatSearchResultCount } from '../../store/utils';
export default {
name: 'ScopeNavigation',
@@ -29,11 +26,7 @@ export default {
methods: {
...mapActions(['fetchSidebarCount']),
showFormatedCount(count) {
- if (!count) {
- return '0';
- }
- const countNumber = parseInt(count.replace(/,/g, ''), 10);
- return formatNumber(countNumber, NUMBER_FORMATING_OPTIONS);
+ return formatSearchResultCount(count);
},
isCountOverLimit(count) {
return count.includes('+');
@@ -82,6 +75,6 @@ export default {
</span>
</gl-nav-item>
</gl-nav>
- <hr class="gl-mt-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" />
+ <hr class="gl-mt-5 gl-mx-5 gl-mb-0 gl-border-gray-100 gl-md-display-none" />
</nav>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue
index eaf7d95822a..4da96a41ef7 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue
@@ -21,6 +21,6 @@ export default {
<template>
<div>
<radio-filter :class="ffBasedXPadding" :filter-data="$options.stateFilterData" />
- <hr class="gl-my-5 gl-border-gray-100" />
+ <hr class="gl-my-5 gl-mx-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index a9c031f91a4..19b1ad0905b 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -1,13 +1,16 @@
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 = [
+export const SCOPE_BLOB = 'blobs';
+export const LABEL_DEFAULT_CLASSES = [
'gl-display-flex',
'gl-flex-direction-row',
'gl-flex-wrap-nowrap',
- 'gl-justify-content-space-between',
'gl-text-gray-900',
];
-
+export const NAV_LINK_DEFAULT_CLASSES = [
+ ...LABEL_DEFAULT_CLASSES,
+ 'gl-justify-content-space-between',
+];
export const NAV_LINK_COUNT_DEFAULT_CLASSES = ['gl-font-sm', 'gl-font-weight-normal'];
+export const HR_DEFAULT_CLASSES = ['gl-my-5', 'gl-mx-5', 'gl-border-gray-100'];
+export const ONLY_SHOW_MD = ['gl-display-none', 'gl-md-display-block'];
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index c6b1257c4ef..415f6f7454c 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -6,11 +6,11 @@ Vue.use(Translate);
export const sidebarInitState = () => {
const el = document.getElementById('js-search-sidebar');
-
if (!el) return {};
- const { navigation } = el.dataset;
- return JSON.parse(navigation);
+ const { navigationJson } = el.dataset;
+ const navigationJsonParsed = JSON.parse(navigationJson);
+ return { navigationJsonParsed };
};
export const initSidebar = (store) => {
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index 678bd82c7a6..e4f67f624ca 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -10,3 +10,5 @@ export const GROUPS_LOCAL_STORAGE_KEY = 'global-search-frequent-groups';
export const PROJECTS_LOCAL_STORAGE_KEY = 'global-search-frequent-projects';
export const SIDEBAR_PARAMS = [stateFilterData.filterParam, confidentialFilterData.filterParam];
+
+export const NUMBER_FORMATING_OPTIONS = { notation: 'compact', compactDisplay: 'short' };
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index f8198017bf8..acb99c60426 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -1,5 +1,12 @@
import AccessorUtilities from '~/lib/utils/accessor';
-import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY, SIDEBAR_PARAMS } from './constants';
+import { formatNumber } from '~/locale';
+import { joinPaths } from '~/lib/utils/url_utility';
+import {
+ MAX_FREQUENT_ITEMS,
+ MAX_FREQUENCY,
+ SIDEBAR_PARAMS,
+ NUMBER_FORMATING_OPTIONS,
+} from './constants';
function extractKeys(object, keyList) {
return Object.fromEntries(keyList.map((key) => [key, object[key]]));
@@ -90,3 +97,18 @@ export const isSidebarDirty = (currentQuery, urlQuery) => {
return userAddedParam || userChangedExistingParam;
});
};
+
+export const formatSearchResultCount = (count) => {
+ if (!count) {
+ return '0';
+ }
+
+ const countNumber = typeof count === 'string' ? parseInt(count.replace(/,/g, ''), 10) : count;
+ return formatNumber(countNumber, NUMBER_FORMATING_OPTIONS);
+};
+
+export const getAggregationsUrl = () => {
+ const currentUrl = new URL(window.location.href);
+ currentUrl.pathname = joinPaths('/search', 'aggregations');
+ return currentUrl.toString();
+};
diff --git a/app/assets/javascripts/search/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
index 121c15199dd..5b1c5819f2b 100644
--- a/app/assets/javascripts/search/topbar/constants.js
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -20,4 +20,4 @@ export const PROJECT_DATA = {
fullName: 'name_with_namespace',
};
-export const SYNTAX_OPTIONS_DOCUMENT = 'drawers/user/search/advanced_search.md';
+export const SYNTAX_OPTIONS_DOCUMENT = 'drawers/drawers/advanced_search_syntax.md';
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
index 7198dbe8b04..d94fd77dd42 100644
--- a/app/assets/javascripts/self_monitor/store/actions.js
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
+import { HTTP_STATUS_ACCEPTED, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { __, s__ } from '~/locale';
import * as types from './mutation_types';
@@ -43,7 +43,7 @@ export const requestCreateProject = ({ dispatch, state, commit }) => {
export const requestCreateProjectStatus = ({ dispatch, state }, jobId) => {
backOffRequest(() => axios.get(state.createProjectStatusEndpoint, { params: { job_id: jobId } }))
.then((resp) => {
- if (resp.status === statusCodes.OK) {
+ if (resp.status === HTTP_STATUS_OK) {
dispatch('requestCreateProjectSuccess', resp.data);
}
})
@@ -95,7 +95,7 @@ export const requestDeleteProject = ({ dispatch, state, commit }) => {
export const requestDeleteProjectStatus = ({ dispatch, state }, jobId) => {
backOffRequest(() => axios.get(state.deleteProjectStatusEndpoint, { params: { job_id: jobId } }))
.then((resp) => {
- if (resp.status === statusCodes.OK) {
+ if (resp.status === HTTP_STATUS_OK) {
dispatch('requestDeleteProjectSuccess', resp.data);
}
})
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index dd27a12cbee..c96189c7cae 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -8,7 +8,6 @@ import {
GlFormInputGroup,
GlDropdown,
GlDropdownItem,
- GlSprintf,
GlFormGroup,
} from '@gitlab/ui';
import $ from 'jquery';
@@ -16,7 +15,8 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { s__ } from '~/locale';
-import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS } from './constants';
+import { formatDate, newDate, nSecondsAfter, isToday } from '~/lib/utils/datetime_utility';
+import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS, NEVER_TIME_RANGE } from './constants';
export default {
components: {
@@ -27,7 +27,6 @@ export default {
GlFormInputGroup,
GlDropdown,
GlDropdownItem,
- GlSprintf,
GlFormGroup,
EmojiPicker: () => import('~/emoji/components/picker.vue'),
},
@@ -56,7 +55,7 @@ export default {
clearStatusAfter: {
type: Object,
required: false,
- default: () => ({}),
+ default: null,
},
currentClearStatusAfter: {
type: String,
@@ -80,6 +79,21 @@ export default {
noEmoji() {
return this.emojiTag === '';
},
+ clearStatusAfterDropdownText() {
+ if (this.clearStatusAfter === null && this.currentClearStatusAfter.length) {
+ return this.formatClearStatusAfterDate(new Date(this.currentClearStatusAfter));
+ }
+
+ if (this.clearStatusAfter?.duration?.seconds) {
+ const clearStatusAfterDate = nSecondsAfter(
+ newDate(),
+ this.clearStatusAfter.duration.seconds,
+ );
+ return this.formatClearStatusAfterDate(clearStatusAfterDate);
+ }
+
+ return NEVER_TIME_RANGE.label;
+ },
},
mounted() {
this.setupEmojiListAndAutocomplete();
@@ -124,6 +138,13 @@ export default {
this.$emit('message-input', '');
this.clearEmoji();
},
+ formatClearStatusAfterDate(date) {
+ if (isToday(date)) {
+ return formatDate(date, 'h:MMtt');
+ }
+
+ return formatDate(date, 'mmm d, yyyy h:MMtt');
+ },
},
TIME_RANGES_WITH_NEVER,
AVAILABILITY_STATUS,
@@ -202,7 +223,7 @@ export default {
<gl-form-group :label="$options.i18n.clearStatusAfterDropdownLabel" class="gl-mb-0">
<gl-dropdown
block
- :text="clearStatusAfter.label"
+ :text="clearStatusAfterDropdownText"
data-testid="clear-status-at-dropdown"
toggle-class="gl-mb-0 gl-form-input-md"
>
@@ -214,14 +235,6 @@ export default {
>{{ after.label }}</gl-dropdown-item
>
</gl-dropdown>
-
- <template v-if="currentClearStatusAfter.length" #description>
- <span data-testid="clear-status-at-message">
- <gl-sprintf :message="$options.i18n.clearStatusAfterMessage">
- <template #date>{{ currentClearStatusAfter }}</template>
- </gl-sprintf>
- </span>
- </template>
</gl-form-group>
</div>
</template>
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 5becc03646e..e7d028e8d23 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -6,8 +6,8 @@ import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { isUserBusy } from './utils';
-import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
+import { isUserBusy, computedClearStatusAfterValue } from './utils';
+import { AVAILABILITY_STATUS } from './constants';
import SetStatusForm from './set_status_form.vue';
Vue.use(GlToast);
@@ -53,9 +53,17 @@ export default {
message: this.currentMessage,
modalId: 'set-user-status-modal',
availability: isUserBusy(this.currentAvailability),
- clearStatusAfter: NEVER_TIME_RANGE,
+ clearStatusAfter: null,
};
},
+ computed: {
+ shouldIncludeClearStatusAfterInApiRequest() {
+ return this.clearStatusAfter !== null;
+ },
+ clearStatusAfterApiRequestValue() {
+ return computedClearStatusAfterValue(this.clearStatusAfter);
+ },
+ },
mounted() {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
@@ -70,14 +78,21 @@ export default {
this.setStatus();
},
setStatus() {
- const { emoji, message, availability, clearStatusAfter } = this;
+ const {
+ emoji,
+ message,
+ availability,
+ shouldIncludeClearStatusAfterInApiRequest,
+ clearStatusAfterApiRequestValue,
+ } = this;
updateUserStatus({
emoji,
message,
availability: availability ? AVAILABILITY_STATUS.BUSY : AVAILABILITY_STATUS.NOT_SET,
- clearStatusAfter:
- clearStatusAfter.label === NEVER_TIME_RANGE.label ? null : clearStatusAfter.shortcut,
+ ...(shouldIncludeClearStatusAfterInApiRequest
+ ? { clearStatusAfter: clearStatusAfterApiRequestValue }
+ : {}),
})
.then(this.onUpdateSuccess)
.catch(this.onUpdateFail);
diff --git a/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue b/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue
index c709611e13d..48693485116 100644
--- a/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/user_profile_set_status_wrapper.vue
@@ -1,9 +1,7 @@
<script>
-import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
-import dateFormat from '~/lib/dateformat';
import SetStatusForm from './set_status_form.vue';
-import { isUserBusy } from './utils';
-import { NEVER_TIME_RANGE, AVAILABILITY_STATUS } from './constants';
+import { isUserBusy, computedClearStatusAfterValue } from './utils';
+import { AVAILABILITY_STATUS } from './constants';
export default {
components: { SetStatusForm },
@@ -13,15 +11,16 @@ export default {
emoji: this.fields.emoji.value,
message: this.fields.message.value,
availability: isUserBusy(this.fields.availability.value),
- clearStatusAfter: NEVER_TIME_RANGE,
+ clearStatusAfter: null,
currentClearStatusAfter: this.fields.clearStatusAfter.value,
};
},
computed: {
- clearStatusAfterInputValue() {
- return this.clearStatusAfter.label === NEVER_TIME_RANGE.label
- ? null
- : this.clearStatusAfter.shortcut;
+ showClearStatusAfterHiddenInput() {
+ return this.clearStatusAfter !== null;
+ },
+ clearStatusAfterHiddenInputValue() {
+ return computedClearStatusAfterValue(this.clearStatusAfter);
},
availabilityInputValue() {
return this.availability
@@ -29,18 +28,6 @@ export default {
: this.$options.AVAILABILITY_STATUS.NOT_SET;
},
},
- mounted() {
- this.$options.formEl = document.querySelector('form.js-edit-user');
-
- if (!this.$options.formEl) return;
-
- this.$options.formEl.addEventListener('ajax:success', this.handleFormSuccess);
- },
- beforeDestroy() {
- if (!this.$options.formEl) return;
-
- this.$options.formEl.removeEventListener('ajax:success', this.handleFormSuccess);
- },
methods: {
handleMessageInput(value) {
this.message = value;
@@ -54,24 +41,6 @@ export default {
handleAvailabilityInput(value) {
this.availability = value;
},
- handleFormSuccess() {
- if (!this.clearStatusAfter?.duration?.seconds) {
- this.currentClearStatusAfter = '';
-
- return;
- }
-
- const now = new Date();
- const currentClearStatusAfterDate = new Date(
- now.getTime() + secondsToMilliseconds(this.clearStatusAfter.duration.seconds),
- );
-
- this.currentClearStatusAfter = dateFormat(
- currentClearStatusAfterDate,
- "UTC:yyyy-mm-dd HH:MM:ss 'UTC'",
- );
- this.clearStatusAfter = NEVER_TIME_RANGE;
- },
},
AVAILABILITY_STATUS,
formEl: null,
@@ -83,7 +52,12 @@ export default {
<input :value="emoji" type="hidden" :name="fields.emoji.name" />
<input :value="message" type="hidden" :name="fields.message.name" />
<input :value="availabilityInputValue" type="hidden" :name="fields.availability.name" />
- <input :value="clearStatusAfterInputValue" type="hidden" :name="fields.clearStatusAfter.name" />
+ <input
+ v-if="showClearStatusAfterHiddenInput"
+ :value="clearStatusAfterHiddenInputValue"
+ type="hidden"
+ :name="fields.clearStatusAfter.name"
+ />
<set-status-form
default-emoji="speech_balloon"
:emoji="emoji"
diff --git a/app/assets/javascripts/set_status_modal/utils.js b/app/assets/javascripts/set_status_modal/utils.js
index 950091195d2..11e47fdf956 100644
--- a/app/assets/javascripts/set_status_modal/utils.js
+++ b/app/assets/javascripts/set_status_modal/utils.js
@@ -1,4 +1,12 @@
-import { AVAILABILITY_STATUS } from './constants';
+import { AVAILABILITY_STATUS, NEVER_TIME_RANGE } from './constants';
export const isUserBusy = (status = '') =>
Boolean(status.length && status.toLowerCase().trim() === AVAILABILITY_STATUS.BUSY);
+
+export const computedClearStatusAfterValue = (value) => {
+ if (value === null || value.name === NEVER_TIME_RANGE.name) {
+ return null;
+ }
+
+ return value.shortcut;
+};
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
index 2a78db352d7..04c62c99a11 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
@@ -1,5 +1,4 @@
<script>
-import $ from 'jquery';
import Vue from 'vue';
import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
import { isInViewport } from '~/lib/utils/common_utils';
@@ -260,8 +259,8 @@ export default {
target?.parentElement?.classList.contains(className),
);
- const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
- (className) => $(target).parents(className).length,
+ const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some((className) =>
+ target?.closest(className),
);
const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target);
diff --git a/app/assets/javascripts/super_sidebar/components/bottom_bar.vue b/app/assets/javascripts/super_sidebar/components/bottom_bar.vue
new file mode 100644
index 00000000000..fea29458f45
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/bottom_bar.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ i18n: {
+ help: __('Help'),
+ new: __('New'),
+ },
+};
+</script>
+
+<template>
+ <div class="bottom-links gl-p-3">
+ <a href="#" class="gl-text-black-normal"
+ ><gl-icon name="question-o" class="gl-mr-3 gl-text-gray-300 gl-text-black-normal!" />{{
+ $options.i18n.help
+ }}</a
+ >
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher.vue b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
new file mode 100644
index 00000000000..f1ddb8290a0
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/context_switcher.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlAvatar, GlSearchBoxByType } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { contextSwitcherItems } from '../mock_data';
+import NavItem from './nav_item.vue';
+
+export default {
+ components: {
+ GlAvatar,
+ GlSearchBoxByType,
+ NavItem,
+ },
+ i18n: {
+ contextNavigation: s__('Navigation|Context navigation'),
+ switchTo: s__('Navigation|Switch to...'),
+ recentProjects: s__('Navigation|Recent projects'),
+ recentGroups: s__('Navigation|Recent groups'),
+ },
+ contextSwitcherItems,
+ viewAllProjectsItem: {
+ title: s__('Navigation|View all projects'),
+ link: '/projects',
+ icon: 'project',
+ },
+ viewAllGroupsItem: {
+ title: s__('Navigation|View all groups'),
+ link: '/groups',
+ icon: 'group',
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-search-box-by-type />
+ <nav :aria-label="$options.i18n.contextNavigation">
+ <ul class="gl-p-0 gl-list-style-none">
+ <li>
+ <div aria-hidden="true" class="gl-font-weight-bold gl-px-3 gl-py-3">
+ {{ $options.i18n.switchTo }}
+ </div>
+ <ul :aria-label="$options.i18n.switchTo" class="gl-p-0">
+ <nav-item :item="$options.contextSwitcherItems.yourWork" />
+ </ul>
+ </li>
+ <li>
+ <div aria-hidden="true" class="gl-font-weight-bold gl-px-3 gl-py-3">
+ {{ $options.i18n.recentProjects }}
+ </div>
+ <ul :aria-label="$options.i18n.recentProjects" class="gl-p-0">
+ <nav-item
+ v-for="project in $options.contextSwitcherItems.recentProjects"
+ :key="project.title"
+ :item="project"
+ >
+ <template #icon>
+ <gl-avatar shape="rect" :size="32" :src="project.avatar" />
+ </template>
+ </nav-item>
+ <nav-item :item="$options.viewAllProjectsItem" />
+ </ul>
+ </li>
+ <li>
+ <div aria-hidden="true" class="gl-font-weight-bold gl-px-3 gl-py-3">
+ {{ $options.i18n.recentGroups }}
+ </div>
+ <ul :aria-label="$options.i18n.recentGroups" class="gl-p-0">
+ <nav-item
+ v-for="project in $options.contextSwitcherItems.recentGroups"
+ :key="project.title"
+ :item="project"
+ >
+ <template #icon>
+ <gl-avatar shape="rect" :size="32" :src="project.avatar" />
+ </template>
+ </nav-item>
+ <nav-item :item="$options.viewAllGroupsItem" />
+ </ul>
+ </li>
+ </ul>
+ </nav>
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
new file mode 100644
index 00000000000..b6f058f7aee
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/context_switcher_toggle.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlTruncate, GlAvatar, GlCollapseToggleDirective, GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlTruncate,
+ GlAvatar,
+ GlIcon,
+ },
+ directives: {
+ CollapseToggle: GlCollapseToggleDirective,
+ },
+ props: {
+ context: {
+ type: Object,
+ required: true,
+ },
+ expanded: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ collapseIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ v-collapse-toggle.context-switcher
+ type="button"
+ class="context-switcher-toggle gl-bg-transparent gl-border-0 border-top border-bottom gl-border-gray-a-08 gl-box-shadow-none gl-display-flex gl-align-items-center gl-font-weight-bold gl-w-full gl-pl-3 gl-pr-5 gl-h-8"
+ >
+ <gl-avatar :size="32" shape="rect" :src="context.avatar" class="gl-mr-3" />
+ <div class="gl-overflow-auto">
+ <gl-truncate :text="context.title" />
+ </div>
+ <span class="gl-flex-grow-1 gl-text-right">
+ <gl-icon :name="collapseIcon" />
+ </span>
+ </button>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
new file mode 100644
index 00000000000..d790e61ca31
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ href: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ icon: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ariaLabel() {
+ return `${this.label} ${this.count}`;
+ },
+ component() {
+ return this.href ? 'a' : 'button';
+ },
+ },
+};
+</script>
+
+<template>
+ <component
+ :is="component"
+ :aria-label="ariaLabel"
+ :href="href"
+ class="counter gl-relative gl-display-inline-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-black-normal gl-border gl-border-gray-a-08 gl-font-sm gl-font-weight-bold"
+ >
+ <gl-icon aria-hidden="true" :name="icon" />
+ <span aria-hidden="true">{{ count }}</span>
+ </component>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
new file mode 100644
index 00000000000..4fd6918fd6f
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ name: 'NavItem',
+ components: {
+ GlIcon,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <a
+ :href="item.link"
+ class="gl-display-flex gl-pl-3 gl-py-3 gl-line-height-normal gl-text-black-normal gl-hover-bg-t-gray-a-08"
+ >
+ <div class="gl-mr-3">
+ <slot name="icon">
+ <gl-icon v-if="item.icon" :name="item.icon" />
+ </slot>
+ </div>
+ <div class="gl-pr-3">
+ {{ item.title }}
+ <div v-if="item.subtitle" class="gl-font-sm gl-text-gray-500 gl-mt-1">
+ {{ item.subtitle }}
+ </div>
+ </div>
+ </a>
+ </li>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
new file mode 100644
index 00000000000..e2eac64f5ad
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlCollapse } from '@gitlab/ui';
+import { context } from '../mock_data';
+import UserBar from './user_bar.vue';
+import ContextSwitcherToggle from './context_switcher_toggle.vue';
+import ContextSwitcher from './context_switcher.vue';
+import BottomBar from './bottom_bar.vue';
+
+export default {
+ context,
+ components: {
+ GlCollapse,
+ UserBar,
+ ContextSwitcherToggle,
+ ContextSwitcher,
+ BottomBar,
+ },
+ props: {
+ sidebarData: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ contextSwitcherOpened: false,
+ };
+ },
+};
+</script>
+
+<template>
+ <aside
+ class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08 gl-z-index-9999"
+ data-testid="super-sidebar"
+ >
+ <user-bar :sidebar-data="sidebarData" />
+ <div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden">
+ <div class="gl-flex-grow-1 gl-overflow-auto">
+ <context-switcher-toggle :context="$options.context" :expanded="contextSwitcherOpened" />
+ <gl-collapse id="context-switcher" v-model="contextSwitcherOpened">
+ <context-switcher />
+ </gl-collapse>
+ </div>
+ <div class="gl-px-3">
+ <bottom-bar />
+ </div>
+ </div>
+ </aside>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
new file mode 100644
index 00000000000..7ee1776bf07
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -0,0 +1,77 @@
+<script>
+import { GlAvatar, GlDropdown, GlIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
+import logo from '../../../../views/shared/_logo.svg';
+import Counter from './counter.vue';
+
+export default {
+ logo,
+ components: {
+ GlAvatar,
+ GlDropdown,
+ GlIcon,
+ NewNavToggle,
+ Counter,
+ },
+ i18n: {
+ issues: __('Issues'),
+ mergeRequests: __('Merge requests'),
+ todoList: __('To-Do list'),
+ },
+ directives: {
+ SafeHtml,
+ },
+ inject: ['rootPath', 'toggleNewNavEndpoint'],
+ props: {
+ sidebarData: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="user-bar">
+ <div class="gl-display-flex gl-align-items-center gl-px-3 gl-py-2 gl-gap-3">
+ <div class="gl-flex-grow-1">
+ <a v-safe-html="$options.logo" :href="rootPath"></a>
+ </div>
+ <gl-dropdown variant="link" no-caret>
+ <template #button-content>
+ <gl-icon name="plus" class="gl-vertical-align-middle gl-text-black-normal" />
+ </template>
+ </gl-dropdown>
+ <button class="gl-border-none">
+ <gl-icon name="search" class="gl-vertical-align-middle" />
+ </button>
+ <gl-dropdown data-testid="user-dropdown" variant="link" no-caret>
+ <template #button-content>
+ <gl-avatar :entity-name="sidebarData.name" :src="sidebarData.avatar_url" :size="32" />
+ </template>
+ <new-nav-toggle :endpoint="toggleNewNavEndpoint" enabled />
+ </gl-dropdown>
+ </div>
+ <div class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2">
+ <counter
+ icon="issues"
+ :count="sidebarData.assigned_open_issues_count"
+ :href="sidebarData.issues_dashboard_path"
+ :label="$options.i18n.issues"
+ />
+ <counter
+ icon="merge-request-open"
+ :count="sidebarData.assigned_open_merge_requests_count"
+ :label="$options.i18n.mergeRequests"
+ />
+ <counter
+ icon="todo-done"
+ :count="sidebarData.todos_pending_count"
+ href="/dashboard/todos"
+ :label="$options.i18n.todoList"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/mock_data.js b/app/assets/javascripts/super_sidebar/mock_data.js
new file mode 100644
index 00000000000..0d1ac006df7
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/mock_data.js
@@ -0,0 +1,59 @@
+import { s__ } from '~/locale';
+
+export const context = {
+ title: 'Typeahead.js',
+ link: '/',
+ avatar: 'https://gitlab.com/uploads/-/system/project/avatar/278964/project_avatar.png?width=32',
+};
+
+export const contextSwitcherItems = {
+ yourWork: { title: s__('Navigation|Your work'), link: '/', icon: 'work' },
+ recentProjects: [
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ title: 'Orange',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/project/avatar/4456656/pajamas-logo.png?width=64',
+ },
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ title: 'Lemon',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar: 'https://gitlab.com/uploads/-/system/project/avatar/7071551/GitLab_UI.png?width=64',
+ },
+ {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ title: 'Coconut',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/project/avatar/4149988/SVGs_project.png?width=64',
+ },
+ ],
+ recentGroups: [
+ {
+ title: 'Developer Evangelism at GitLab',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/group/avatar/10087220/rainbow_tanuki.jpg?width=64',
+ },
+ {
+ title: 'security-products',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/group/avatar/11932235/gitlab-icon-rgb.png?width=64',
+ },
+ {
+ title: 'Tanuki-Workshops',
+ subtitle: 'tropical-tree',
+ link: '/tropical-tree',
+ avatar:
+ 'https://gitlab.com/uploads/-/system/group/avatar/5085244/Screenshot_2019-04-29_at_16.13.07.png?width=64',
+ },
+ ],
+};
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
new file mode 100644
index 00000000000..b9c7073df8c
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import SuperSidebar from './components/super_sidebar.vue';
+
+export const initSuperSidebar = () => {
+ const el = document.querySelector('.js-super-sidebar');
+
+ if (!el) return false;
+
+ const { rootPath, sidebar, toggleNewNavEndpoint } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'SuperSidebarRoot',
+ provide: {
+ rootPath,
+ toggleNewNavEndpoint,
+ },
+ render(h) {
+ return h(SuperSidebar, {
+ props: {
+ sidebarData: JSON.parse(sidebar),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 96b6e78c668..cb2bf24abc7 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -11,7 +11,7 @@
//
export default function syntaxHighlight($els = null) {
- if (!$els) return;
+ if (!$els || $els.length === 0) return;
const els = $els.get ? $els.get() : $els;
const handler = (el) => {
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index b19f92aaeb4..c88c528a632 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -11,14 +11,14 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, sprintf } from '~/locale';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import StateActions from './states_table_actions.vue';
export default {
components: {
- CiBadge,
+ CiBadgeLink,
GlAlert,
GlBadge,
GlLink,
@@ -198,7 +198,7 @@ export default {
:id="`terraformJobStatusContainer${item.name}`"
class="gl-my-2"
>
- <ci-badge
+ <ci-badge-link
:id="`terraformJobStatus${item.name}`"
:status="pipelineDetailedStatus(item)"
class="gl-py-1"
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
new file mode 100644
index 00000000000..94bc15fa0d0
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlAlert, GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import { updateRepositorySize } from '~/api/projects_api';
+import {
+ ERROR_MESSAGE,
+ LEARN_MORE_LABEL,
+ USAGE_QUOTAS_LABEL,
+ TOTAL_USAGE_TITLE,
+ TOTAL_USAGE_SUBTITLE,
+ TOTAL_USAGE_DEFAULT_TEXT,
+ HELP_LINK_ARIA_LABEL,
+ RECALCULATE_REPOSITORY_LABEL,
+ projectContainerRegistryPopoverContent,
+} from '../constants';
+import getProjectStorageStatistics from '../queries/project_storage.query.graphql';
+import { parseGetProjectStorageResults } from '../utils';
+import UsageGraph from './usage_graph.vue';
+import ProjectStorageDetail from './project_storage_detail.vue';
+
+export default {
+ name: 'ProjectStorageApp',
+ components: {
+ GlAlert,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ UsageGraph,
+ ProjectStorageDetail,
+ },
+ inject: ['projectPath', 'helpLinks'],
+ provide: {
+ containerRegistryPopoverContent: projectContainerRegistryPopoverContent,
+ },
+ apollo: {
+ project: {
+ query: getProjectStorageStatistics,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return parseGetProjectStorageResults(data, this.helpLinks);
+ },
+ error() {
+ this.error = ERROR_MESSAGE;
+ },
+ },
+ },
+ data() {
+ return {
+ project: {},
+ error: '',
+ loadingRecalculateSize: false,
+ };
+ },
+ computed: {
+ totalUsage() {
+ return this.project?.storage?.totalUsage || TOTAL_USAGE_DEFAULT_TEXT;
+ },
+ storageTypes() {
+ return this.project?.storage?.storageTypes || [];
+ },
+ },
+ methods: {
+ clearError() {
+ this.error = '';
+ },
+ helpLinkAriaLabel(linkTitle) {
+ return sprintf(HELP_LINK_ARIA_LABEL, {
+ linkTitle,
+ });
+ },
+ async postRecalculateSize() {
+ const alertEl = document.querySelector('.js-recalculation-started-alert');
+
+ this.loadingRecalculateSize = true;
+
+ await updateRepositorySize(this.projectPath);
+
+ this.loadingRecalculateSize = false;
+ alertEl?.classList.remove('gl-display-none');
+ },
+ },
+ LEARN_MORE_LABEL,
+ USAGE_QUOTAS_LABEL,
+ TOTAL_USAGE_TITLE,
+ TOTAL_USAGE_SUBTITLE,
+ RECALCULATE_REPOSITORY_LABEL,
+};
+</script>
+<template>
+ <gl-loading-icon v-if="$apollo.queries.project.loading" class="gl-mt-5" size="lg" />
+ <gl-alert v-else-if="error" variant="danger" @dismiss="clearError">
+ {{ error }}
+ </gl-alert>
+ <div v-else>
+ <div class="gl-pt-5 gl-px-3">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div>
+ <p class="gl-m-0 gl-font-lg gl-font-weight-bold">{{ $options.TOTAL_USAGE_TITLE }}</p>
+ <p class="gl-m-0 gl-text-gray-400">
+ {{ $options.TOTAL_USAGE_SUBTITLE }}
+ <gl-link
+ :href="helpLinks.usageQuotas"
+ target="_blank"
+ :aria-label="helpLinkAriaLabel($options.USAGE_QUOTAS_LABEL)"
+ data-testid="usage-quotas-help-link"
+ >
+ {{ $options.LEARN_MORE_LABEL }}
+ </gl-link>
+ </p>
+ </div>
+ <p class="gl-m-0 gl-font-size-h-display gl-font-weight-bold" data-testid="total-usage">
+ {{ totalUsage }}
+ </p>
+ </div>
+ </div>
+ <div v-if="project.statistics" class="gl-w-full">
+ <usage-graph :root-storage-statistics="project.statistics" :limit="0" />
+ </div>
+ <div class="gl-w-full gl-my-5">
+ <gl-button
+ :loading="loadingRecalculateSize"
+ category="secondary"
+ @click="postRecalculateSize"
+ >
+ {{ $options.RECALCULATE_REPOSITORY_LABEL }}
+ </gl-button>
+ </div>
+ <project-storage-detail :storage-types="storageTypes" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue
new file mode 100644
index 00000000000..2b97886e650
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue
@@ -0,0 +1,142 @@
+<script>
+import { GlIcon, GlLink, GlSprintf, GlTableLite, GlPopover } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { thWidthPercent } from '~/lib/utils/table_utility';
+import { sprintf } from '~/locale';
+import {
+ HELP_LINK_ARIA_LABEL,
+ PROJECT_TABLE_LABEL_STORAGE_TYPE,
+ PROJECT_TABLE_LABEL_USAGE,
+ containerRegistryId,
+ containerRegistryPopoverId,
+ uploadsId,
+ uploadsPopoverId,
+ uploadsPopoverContent,
+} from '../constants';
+import { descendingStorageUsageSort } from '../utils';
+import StorageTypeIcon from './storage_type_icon.vue';
+
+export default {
+ name: 'ProjectStorageDetail',
+ components: {
+ GlLink,
+ GlIcon,
+ GlTableLite,
+ GlSprintf,
+ StorageTypeIcon,
+ GlPopover,
+ },
+ inject: ['containerRegistryPopoverContent'],
+ props: {
+ storageTypes: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ sizeSortedStorageTypes() {
+ const warnings = {
+ [containerRegistryId]: {
+ popoverId: containerRegistryPopoverId,
+ popoverContent: this.containerRegistryPopoverContent,
+ },
+ [uploadsId]: {
+ popoverId: uploadsPopoverId,
+ popoverContent: this.$options.i18n.uploadsPopoverContent,
+ },
+ };
+
+ return this.storageTypes
+ .map((type) => {
+ const warning = warnings[type.storageType.id] || null;
+ return {
+ warning,
+ ...type,
+ };
+ })
+ .sort(descendingStorageUsageSort('value'));
+ },
+ },
+ methods: {
+ helpLinkAriaLabel(linkTitle) {
+ return sprintf(HELP_LINK_ARIA_LABEL, {
+ linkTitle,
+ });
+ },
+ numberToHumanSize,
+ },
+ projectTableFields: [
+ {
+ key: 'storageType',
+ label: PROJECT_TABLE_LABEL_STORAGE_TYPE,
+ thClass: thWidthPercent(90),
+ },
+ {
+ key: 'value',
+ label: PROJECT_TABLE_LABEL_USAGE,
+ thClass: thWidthPercent(10),
+ },
+ ],
+ i18n: {
+ uploadsPopoverContent,
+ },
+};
+</script>
+<template>
+ <gl-table-lite :items="sizeSortedStorageTypes" :fields="$options.projectTableFields">
+ <template #cell(storageType)="{ item }">
+ <div class="gl-display-flex gl-flex-direction-row">
+ <storage-type-icon
+ :name="item.storageType.id"
+ :data-testid="`${item.storageType.id}-icon`"
+ />
+ <div>
+ <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
+ {{ item.storageType.name }}
+ <gl-link
+ v-if="item.storageType.helpPath"
+ :href="item.storageType.helpPath"
+ target="_blank"
+ :aria-label="helpLinkAriaLabel(item.storageType.name)"
+ :data-testid="`${item.storageType.id}-help-link`"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </p>
+ <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
+ {{ item.storageType.description }}
+ </p>
+ <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
+ <gl-icon name="warning" :size="12" />
+ <gl-sprintf :message="item.storageType.warningMessage">
+ <template #warningLink="{ content }">
+ <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </div>
+ </div>
+ </template>
+
+ <template #cell(value)="{ item }">
+ {{ numberToHumanSize(item.value, 1) }}
+
+ <template v-if="item.warning">
+ <gl-icon
+ :id="item.warning.popoverId"
+ name="warning"
+ class="gl-mt-2 gl-lg-mt-0 gl-lg-ml-2"
+ />
+ <gl-popover
+ triggers="hover focus"
+ placement="top"
+ :target="item.warning.popoverId"
+ :content="item.warning.popoverContent"
+ :data-testid="item.warning.popoverId"
+ />
+ </template>
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_type_icon.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_type_icon.vue
new file mode 100644
index 00000000000..bc7cd42df1e
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/storage_type_icon.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+
+export default {
+ components: { GlIcon },
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ methods: {
+ iconName(storageTypeName) {
+ const defaultStorageTypeIcon = 'disk';
+ const storageTypeIconMap = {
+ lfsObjectsSize: 'doc-image',
+ snippetsSize: 'snippet',
+ uploadsSize: 'upload',
+ repositorySize: 'infrastructure-registry',
+ packagesSize: 'package',
+ };
+
+ return storageTypeIconMap[`${storageTypeName}`] ?? defaultStorageTypeIcon;
+ },
+ },
+};
+</script>
+<template>
+ <span
+ class="gl-display-inline-flex gl-align-items-flex-start gl-justify-content-center gl-min-w-8 gl-pr-2 gl-pt-1"
+ >
+ <gl-icon :name="iconName(name)" :size="16" class="gl-mt-1" />
+ </span>
+</template>
diff --git a/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue b/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue
new file mode 100644
index 00000000000..7e001685060
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/usage_graph.vue
@@ -0,0 +1,170 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { PROJECT_STORAGE_TYPES } from '../constants';
+import { descendingStorageUsageSort } from '../utils';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ rootStorageStatistics: {
+ required: true,
+ type: Object,
+ },
+ limit: {
+ required: true,
+ type: Number,
+ },
+ },
+ computed: {
+ storageTypes() {
+ const {
+ containerRegistrySize,
+ buildArtifactsSize,
+ pipelineArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ storageSize,
+ wikiSize,
+ snippetsSize,
+ uploadsSize,
+ } = this.rootStorageStatistics;
+ const artifactsSize = buildArtifactsSize + pipelineArtifactsSize;
+
+ if (storageSize === 0) {
+ return null;
+ }
+
+ return [
+ {
+ id: 'repositorySize',
+ style: this.usageStyle(this.barRatio(repositorySize)),
+ class: 'gl-bg-data-viz-blue-500',
+ size: repositorySize,
+ },
+ {
+ id: 'lfsObjectsSize',
+ style: this.usageStyle(this.barRatio(lfsObjectsSize)),
+ class: 'gl-bg-data-viz-orange-600',
+ size: lfsObjectsSize,
+ },
+ {
+ id: 'packagesSize',
+ style: this.usageStyle(this.barRatio(packagesSize)),
+ class: 'gl-bg-data-viz-aqua-500',
+ size: packagesSize,
+ },
+ {
+ id: 'containerRegistrySize',
+ style: this.usageStyle(this.barRatio(containerRegistrySize)),
+ class: 'gl-bg-data-viz-aqua-800',
+ size: containerRegistrySize,
+ },
+ {
+ id: 'buildArtifactsSize',
+ style: this.usageStyle(this.barRatio(artifactsSize)),
+ class: 'gl-bg-data-viz-green-600',
+ size: artifactsSize,
+ },
+ {
+ id: 'wikiSize',
+ style: this.usageStyle(this.barRatio(wikiSize)),
+ class: 'gl-bg-data-viz-magenta-500',
+ size: wikiSize,
+ },
+ {
+ id: 'snippetsSize',
+ style: this.usageStyle(this.barRatio(snippetsSize)),
+ class: 'gl-bg-data-viz-orange-800',
+ size: snippetsSize,
+ },
+ {
+ id: 'uploadsSize',
+ style: this.usageStyle(this.barRatio(uploadsSize)),
+ class: 'gl-bg-data-viz-aqua-700',
+ size: uploadsSize,
+ },
+ ]
+ .filter((data) => data.size !== 0)
+ .sort(descendingStorageUsageSort('size'))
+ .map((storageType) => {
+ const storageTypeExtraData = PROJECT_STORAGE_TYPES.find(
+ (type) => storageType.id === type.id,
+ );
+ const { name, tooltip } = storageTypeExtraData || {};
+
+ return {
+ name,
+ tooltip,
+ ...storageType,
+ };
+ });
+ },
+ },
+ methods: {
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ usageStyle(ratio) {
+ return { flex: ratio };
+ },
+ barRatio(size) {
+ let max = this.rootStorageStatistics.storageSize;
+
+ if (this.limit !== 0 && max <= this.limit) {
+ max = this.limit;
+ }
+
+ return size / max;
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
+ <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
+ <div
+ v-for="storageType in storageTypes"
+ :key="storageType.name"
+ class="storage-type-usage gl-h-full gl-display-inline-block"
+ :class="storageType.class"
+ :style="storageType.style"
+ data-testid="storage-type-usage"
+ ></div>
+ </div>
+ <div class="row gl-mb-4">
+ <div
+ v-for="storageType in storageTypes"
+ :key="storageType.name"
+ class="col-md-auto gl-display-flex gl-align-items-center"
+ data-testid="storage-type-legend"
+ data-qa-selector="storage_type_legend"
+ >
+ <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
+ <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
+ {{ storageType.name }}
+ </span>
+ <span class="gl-text-gray-500 gl-font-sm">
+ {{ formatSize(storageType.size) }}
+ </span>
+ <span
+ v-if="storageType.tooltip"
+ v-gl-tooltip
+ :title="storageType.tooltip"
+ :aria-label="storageType.tooltip"
+ class="gl-ml-2"
+ >
+ <gl-icon name="question" :size="12" />
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/usage_quotas/storage/constants.js b/app/assets/javascripts/usage_quotas/storage/constants.js
new file mode 100644
index 00000000000..fab18cefc60
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/constants.js
@@ -0,0 +1,100 @@
+import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export const ERROR_MESSAGE = s__(
+ 'UsageQuota|Something went wrong while fetching project storage statistics',
+);
+export const LEARN_MORE_LABEL = __('Learn more.');
+export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas');
+export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage breakdown');
+export const TOTAL_USAGE_SUBTITLE = s__(
+ 'UsageQuota|Includes artifacts, repositories, wiki, uploads, and other items.',
+);
+export const TOTAL_USAGE_DEFAULT_TEXT = __('Not applicable.');
+export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link');
+export const RECALCULATE_REPOSITORY_LABEL = s__('UsageQuota|Recalculate repository usage');
+
+export const projectContainerRegistryPopoverContent = s__(
+ 'UsageQuotas|The project-level storage statistics for the Container Registry are directional only and do not include savings for instance-wide deduplication.',
+);
+
+export const containerRegistryId = 'containerRegistrySize';
+export const containerRegistryPopoverId = 'container-registry-popover';
+export const uploadsId = 'uploadsSize';
+export const uploadsPopoverId = 'uploads-popover';
+export const uploadsPopoverContent = s__(
+ 'NamespaceStorage|Uploads are not counted in namespace storage quotas.',
+);
+
+export const PROJECT_TABLE_LABEL_PROJECT = __('Project');
+export const PROJECT_TABLE_LABEL_STORAGE_TYPE = s__('UsageQuota|Storage type');
+export const PROJECT_TABLE_LABEL_USAGE = s__('UsageQuota|Usage');
+
+export const PROJECT_STORAGE_TYPES = [
+ {
+ id: 'containerRegistrySize',
+ name: s__('UsageQuota|Container Registry'),
+ description: s__(
+ 'UsageQuota|Gitlab-integrated Docker Container Registry for storing Docker Images.',
+ ),
+ },
+ {
+ id: 'buildArtifactsSize',
+ name: s__('UsageQuota|Artifacts'),
+ description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'),
+ tooltip: s__('UsageQuota|Artifacts is a sum of build and pipeline artifacts.'),
+ },
+ {
+ id: 'lfsObjectsSize',
+ name: s__('UsageQuota|LFS storage'),
+ description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'),
+ },
+ {
+ id: 'packagesSize',
+ name: s__('UsageQuota|Packages'),
+ description: s__('UsageQuota|Code packages and container images.'),
+ },
+ {
+ id: 'repositorySize',
+ name: s__('UsageQuota|Repository'),
+ description: s__('UsageQuota|Git repository.'),
+ },
+ {
+ id: 'snippetsSize',
+ name: s__('UsageQuota|Snippets'),
+ description: s__('UsageQuota|Shared bits of code and text.'),
+ },
+ {
+ id: 'uploadsSize',
+ name: s__('UsageQuota|Uploads'),
+ description: s__('UsageQuota|File attachments and smaller design graphics.'),
+ },
+ {
+ id: 'wikiSize',
+ name: s__('UsageQuota|Wiki'),
+ description: s__('UsageQuota|Wiki content.'),
+ },
+];
+
+export const projectHelpPaths = {
+ containerRegistry: helpPagePath(
+ 'user/packages/container_registry/reduce_container_registry_storage',
+ ),
+ usageQuotas: helpPagePath('user/usage_quotas'),
+ usageQuotasNamespaceStorageLimit: helpPagePath('user/usage_quotas', {
+ anchor: 'namespace-storage-limit',
+ }),
+ buildArtifacts: helpPagePath('ci/pipelines/job_artifacts', {
+ anchor: 'when-job-artifacts-are-deleted',
+ }),
+ packages: helpPagePath('user/packages/package_registry/index.md', {
+ anchor: 'reduce-storage-usage',
+ }),
+ repository: helpPagePath('user/project/repository/reducing_the_repo_size_using_git'),
+ snippets: helpPagePath('user/snippets', {
+ anchor: 'reduce-snippets-repository-size',
+ }),
+ wiki: helpPagePath('administration/wikis/index.md', {
+ anchor: 'reduce-wiki-repository-size',
+ }),
+};
diff --git a/app/assets/javascripts/usage_quotas/storage/init_project_storage.js b/app/assets/javascripts/usage_quotas/storage/init_project_storage.js
new file mode 100644
index 00000000000..00cb274902d
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/init_project_storage.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { projectHelpPaths as helpLinks } from './constants';
+import ProjectStorageApp from './components/project_storage_app.vue';
+
+Vue.use(VueApollo);
+
+export default (containerId = 'js-project-storage-count-app') => {
+ const el = document.getElementById(containerId);
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectPath } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ name: 'ProjectStorageApp',
+ provide: {
+ projectPath,
+ helpLinks,
+ },
+ render(createElement) {
+ return createElement(ProjectStorageApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql b/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql
new file mode 100644
index 00000000000..6637e5e0865
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/queries/project_storage.query.graphql
@@ -0,0 +1,17 @@
+query getProjectStorageStatistics($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ statistics {
+ containerRegistrySize
+ buildArtifactsSize
+ pipelineArtifactsSize
+ lfsObjectsSize
+ packagesSize
+ repositorySize
+ snippetsSize
+ storageSize
+ uploadsSize
+ wikiSize
+ }
+ }
+}
diff --git a/app/assets/javascripts/usage_quotas/storage/utils.js b/app/assets/javascripts/usage_quotas/storage/utils.js
new file mode 100644
index 00000000000..443788f650d
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/utils.js
@@ -0,0 +1,49 @@
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { PROJECT_STORAGE_TYPES } from './constants';
+
+export const getStorageTypesFromProjectStatistics = (projectStatistics, helpLinks = {}) =>
+ PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
+ const helpPathKey = currentType.id.replace(`Size`, ``);
+ const helpPath = helpLinks[helpPathKey];
+
+ return types.concat({
+ storageType: {
+ ...currentType,
+ helpPath,
+ },
+ value: projectStatistics[currentType.id],
+ });
+ }, []);
+
+/**
+ * This method parses the results from `getProjectStorageStatistics` call.
+ *
+ * @param {Object} data graphql result
+ * @returns {Object}
+ */
+export const parseGetProjectStorageResults = (data, helpLinks) => {
+ const projectStatistics = data?.project?.statistics;
+ if (!projectStatistics) {
+ return {};
+ }
+ const { storageSize } = projectStatistics;
+ const storageTypes = getStorageTypesFromProjectStatistics(projectStatistics, helpLinks);
+
+ return {
+ storage: {
+ totalUsage: numberToHumanSize(storageSize, 1),
+ storageTypes,
+ },
+ statistics: projectStatistics,
+ };
+};
+
+/**
+ * Creates a sorting function to sort storage types by usage in the graph and in the table
+ *
+ * @param {string} storageUsageKey key storing value of storage usage
+ * @returns {Function} sorting function
+ */
+export function descendingStorageUsageSort(storageUsageKey) {
+ return (a, b) => b[storageUsageKey] - a[storageUsageKey];
+}
diff --git a/app/assets/javascripts/users/profile/components/report_abuse_button.vue b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
new file mode 100644
index 00000000000..aabb7fde396
--- /dev/null
+++ b/app/assets/javascripts/users/profile/components/report_abuse_button.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+export default {
+ name: 'ReportAbuseButton',
+ components: {
+ GlButton,
+ AbuseCategorySelector,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ reportAbuse: __('Report abuse to administrator'),
+ },
+ data() {
+ return {
+ open: false,
+ };
+ },
+ computed: {
+ buttonTooltipText() {
+ return this.$options.i18n.reportAbuse;
+ },
+ },
+ methods: {
+ openDrawer() {
+ this.open = true;
+ },
+ closeDrawer() {
+ this.open = false;
+ },
+ hideTooltips() {
+ this.$root.$emit(BV_HIDE_TOOLTIP);
+ },
+ },
+};
+</script>
+<template>
+ <span>
+ <gl-button
+ v-gl-tooltip="buttonTooltipText"
+ category="primary"
+ :aria-label="buttonTooltipText"
+ icon="error"
+ @click="openDrawer"
+ @mouseout="hideTooltips"
+ />
+ <abuse-category-selector :show-drawer="open" @close-drawer="closeDrawer" />
+ </span>
+</template>
diff --git a/app/assets/javascripts/users/profile/index.js b/app/assets/javascripts/users/profile/index.js
new file mode 100644
index 00000000000..37f8e3ac471
--- /dev/null
+++ b/app/assets/javascripts/users/profile/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import ReportAbuseButton from './components/report_abuse_button.vue';
+
+export const initReportAbuse = () => {
+ const el = document.getElementById('js-report-abuse');
+
+ if (!el) return false;
+
+ const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset;
+
+ return new Vue({
+ el,
+ provide: { reportAbusePath, reportedUserId, reportedFromUrl },
+ render(createElement) {
+ return createElement(ReportAbuseButton);
+ },
+ });
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index e8cc9b2eb2a..7cfc9431c2a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
@@ -69,7 +69,7 @@ export default {
isCollapsible() {
if (!this.isLoadingSummary && this.loadingState !== LOADING_STATES.collapsedError) {
if (this.shouldCollapse) {
- return this.shouldCollapse();
+ return this.shouldCollapse(this.collapsedData);
}
return true;
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 6475def461a..e435dc56503 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
@@ -1,12 +1,10 @@
<script>
-import { GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlButton,
- GlLoadingIcon,
- GlIcon,
},
props: {
title: {
@@ -32,7 +30,7 @@ export default {
computed: {
arrowIconName() {
- return this.isCollapsed ? 'chevron-lg-right' : 'chevron-lg-down';
+ return this.isCollapsed ? 'chevron-right' : 'chevron-down';
},
ariaLabel() {
return this.isCollapsed ? __('Expand') : __('Collapse');
@@ -47,7 +45,7 @@ export default {
</script>
<template>
<div class="mr-widget-extension">
- <div class="d-flex align-items-center pl-3">
+ <div class="d-flex align-items-center pl-3 gl-py-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">
@@ -57,16 +55,15 @@ export default {
</div>
<template v-else>
- <button
- class="btn-blank btn s32 square"
- type="button"
+ <gl-button
+ class="gl-mr-3"
+ size="small"
:aria-label="ariaLabel"
- :disabled="isLoading"
+ :loading="isLoading"
+ :icon="arrowIconName"
+ category="tertiary"
@click="toggleCollapsed"
- >
- <gl-loading-icon v-if="isLoading" size="sm" />
- <gl-icon v-else :name="arrowIconName" class="js-icon" />
- </button>
+ />
<template v-if="isCollapsed">
<slot name="header"></slot>
<gl-button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
index c2a3ae361ca..20284c4a3d8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_how_to_merge_modal.vue
@@ -19,20 +19,18 @@ export default {
},
step3: {
label: __('Step 3.'),
- help: __(
- 'Merge the feature branch into the target branch and fix any conflicts. %{linkStart}How do I fix them?%{linkEnd}',
- ),
+ help: __('Resolve any conflicts. %{linkStart}How do I fix them?%{linkEnd}'),
},
step4: {
label: __('Step 4.'),
- help: __('Push the target branch up to GitLab.'),
+ help: __('Push the source branch up to GitLab.'),
},
},
copyCommands: __('Copy commands'),
tip: __(
- '%{strongStart}Tip:%{strongEnd} You can also check out merge requests locally. %{linkStart}Learn more.%{linkEnd}',
+ '%{strongStart}Tip:%{strongEnd} You can also %{linkStart}check out with merge request ID%{linkEnd}.',
),
- title: __('Check out, review, and merge locally'),
+ title: __('Check out, review, and resolve locally'),
},
components: {
GlModal,
@@ -93,21 +91,11 @@ export default {
: `git fetch origin\ngit checkout -b ${this.escapedSourceBranch} ${escapedOriginBranch}`;
},
mergeInfo2() {
- return this.isFork
- ? `git fetch origin\ngit checkout ${this.escapedTargetBranch}\ngit merge --no-ff ${this.escapedForkBranch}`
- : `git fetch origin\ngit checkout ${this.escapedTargetBranch}\ngit merge --no-ff ${this.escapedSourceBranch}`;
- },
- mergeInfo3() {
- return this.canMerge
- ? `git push origin ${this.escapedTargetBranch}`
- : __('Note that pushing to GitLab requires write access to this repository.');
+ return `git push origin ${this.escapedSourceBranch}`;
},
escapedForkBranch() {
return escapeShellString(`${this.sourceProjectPath}-${this.sourceBranch}`);
},
- escapedTargetBranch() {
- return escapeShellString(this.targetBranch);
- },
escapedSourceBranch() {
return escapeShellString(this.sourceBranch);
},
@@ -145,6 +133,18 @@ export default {
class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
/>
</div>
+ <p v-if="reviewingDocsPath">
+ <gl-sprintf data-testid="docs-tip" :message="$options.i18n.tip">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #link="{ content }">
+ <gl-link class="gl-display-inline-block" :href="reviewingDocsPath" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
<p>
<strong>
@@ -164,14 +164,6 @@ export default {
</template>
</gl-sprintf>
</p>
- <div class="gl-display-flex">
- <pre class="gl-w-full" data-testid="how-to-merge-instructions">{{ mergeInfo2 }}</pre>
- <clipboard-button
- :text="mergeInfo2"
- :title="$options.i18n.copyCommands"
- class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
- />
- </div>
<p>
<strong>
{{ $options.i18n.steps.step4.label }}
@@ -179,24 +171,12 @@ export default {
{{ $options.i18n.steps.step4.help }}
</p>
<div class="gl-display-flex">
- <pre class="gl-w-full" data-testid="how-to-merge-instructions">{{ mergeInfo3 }}</pre>
+ <pre class="gl-w-full" data-testid="how-to-merge-instructions">{{ mergeInfo2 }}</pre>
<clipboard-button
- :text="mergeInfo3"
+ :text="mergeInfo2"
:title="$options.i18n.copyCommands"
class="gl-shadow-none! gl-bg-transparent! gl-flex-shrink-0"
/>
</div>
- <p v-if="reviewingDocsPath">
- <gl-sprintf data-testid="docs-tip" :message="$options.i18n.tip">
- <template #strong="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link class="gl-display-inline-block" :href="reviewingDocsPath" target="_blank">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
</gl-modal>
</template>
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
index 2683956e603..ecf08f78f57 100644
--- 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
@@ -6,7 +6,15 @@ export default {
};
},
updated() {
- this.hasChildren = this.$scopedSlots.default?.()?.some((c) => c.tag);
+ this.hasChildren = this.checkSlots();
+ },
+ mounted() {
+ this.hasChildren = this.checkSlots();
+ },
+ methods: {
+ checkSlots() {
+ return this.$scopedSlots.default?.()?.some((c) => c.tag);
+ },
},
};
</script>
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 9a3555d3e11..f7d6f7b4345 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
@@ -76,17 +76,17 @@ export default {
<div
:class="{
'gl-display-flex gl-align-items-center': actions.length,
- 'gl-md-display-flex gl-align-items-center': !actions.length,
+ 'gl-md-display-flex gl-align-items-center gl-flex-wrap gl-gap-3': !actions.length,
}"
- class="media-body"
+ class="media-body gl-line-height-24"
>
<slot></slot>
<div
:class="{
- 'state-container-action-buttons gl-flex-direction-column gl-flex-wrap gl-justify-content-end': !actions.length,
+ 'state-container-action-buttons gl-flex-wrap gl-lg-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"
+ class="gl-display-flex gl-font-size-0 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/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 8e1b18c63a4..a5d982fe221 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
@@ -88,25 +88,24 @@ export default {
</template>
<template v-if="!isLoading && !state.shouldBeRebased" #actions>
<gl-button
- v-if="userPermissions.canMerge"
+ v-if="showResolveButton"
+ :href="mr.conflictResolutionPath"
size="small"
variant="confirm"
- category="secondary"
- data-testid="merge-locally-button"
- class="js-check-out-modal-trigger gl-align-self-start"
- :class="{ 'gl-mr-2': showResolveButton }"
+ class="gl-align-self-start"
+ data-testid="resolve-conflicts-button"
>
- {{ s__('mrWidget|Resolve locally') }}
+ {{ s__('mrWidget|Resolve conflicts') }}
</gl-button>
<gl-button
- v-if="showResolveButton"
- :href="mr.conflictResolutionPath"
+ v-if="userPermissions.canMerge"
size="small"
variant="confirm"
- class="gl-mb-2 gl-md-mb-0 gl-align-self-start"
- data-testid="resolve-conflicts-button"
+ category="secondary"
+ data-testid="merge-locally-button"
+ class="js-check-out-modal-trigger gl-align-self-start"
>
- {{ s__('mrWidget|Resolve conflicts') }}
+ {{ s__('mrWidget|Resolve locally') }}
</gl-button>
</template>
</state-container>
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 4ae4edf02c3..d687f0346c7 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
@@ -179,27 +179,27 @@ export default {
</template>
<template v-if="!isLoading" #actions>
<gl-button
- v-if="showRebaseWithoutPipeline"
:loading="isMakingRequest"
variant="confirm"
size="small"
- category="secondary"
- data-testid="rebase-without-ci-button"
- class="gl-align-self-start gl-mr-2"
- @click="rebaseWithoutCi"
+ data-qa-selector="mr_rebase_button"
+ data-testid="standard-rebase-button"
+ class="gl-align-self-start"
+ @click="rebase"
>
- {{ __('Rebase without pipeline') }}
+ {{ __('Rebase') }}
</gl-button>
<gl-button
+ v-if="showRebaseWithoutPipeline"
:loading="isMakingRequest"
variant="confirm"
size="small"
- data-qa-selector="mr_rebase_button"
- data-testid="standard-rebase-button"
- class="gl-mb-2 gl-md-mb-0 gl-align-self-start"
- @click="rebase"
+ category="secondary"
+ data-testid="rebase-without-ci-button"
+ class="gl-align-self-start gl-mr-2"
+ @click="rebaseWithoutCi"
>
- {{ __('Rebase') }}
+ {{ __('Rebase without pipeline') }}
</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 01f9b4757a0..211fbba305f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
import { produce } from 'immer';
-import $ from 'jquery';
import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
@@ -111,7 +110,9 @@ export default {
},
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
- $('.merge-request .detail-page-description .title').text(title);
+ document.querySelector(
+ '.merge-request .detail-page-description .title',
+ ).textContent = title;
if (!window.gon?.features?.realtimeMrStatusChange) {
eventHub.$emit('MRWidgetUpdateRequested');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
index 18aa85484ea..5db5f1f8dcf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue
@@ -1,5 +1,11 @@
<script>
export default {
+ components: {
+ MrSecurityWidget: () =>
+ import(
+ '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue'
+ ),
+ },
props: {
mr: {
type: Object,
@@ -8,7 +14,9 @@ export default {
},
computed: {
widgets() {
- return [].filter((w) => w);
+ return [window.gon?.features?.refactorSecurityExtension && 'MrSecurityWidget'].filter(
+ (w) => w,
+ );
},
},
};
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 cdf35033021..7343c98938c 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
@@ -42,7 +42,8 @@ export default {
*/
value: {
type: Object,
- required: true,
+ required: false,
+ default: () => ({}),
},
loadingText: {
type: String,
@@ -56,7 +57,8 @@ export default {
},
fetchCollapsedData: {
type: Function,
- required: true,
+ required: false,
+ default: undefined,
},
fetchExpandedData: {
type: Function,
@@ -119,6 +121,12 @@ export default {
required: false,
default: null,
},
+ // When this is provided, the widget will display an error message in the summary section.
+ hasError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -138,8 +146,17 @@ export default {
summaryStatusIcon() {
return this.summaryError ? this.$options.failedStatusIcon : this.statusIconName;
},
+ hasActionButtons() {
+ return this.actionButtons.length > 0 || Boolean(this.$scopedSlots['action-buttons']);
+ },
},
watch: {
+ hasError: {
+ handler(newValue) {
+ this.summaryError = newValue ? this.errorText : null;
+ },
+ immediate: true,
+ },
isLoading(newValue) {
this.$emit('is-loading', newValue);
},
@@ -154,7 +171,9 @@ export default {
this.telemetryHub?.viewed();
try {
- await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
+ if (this.fetchCollapsedData) {
+ await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
+ }
} catch {
this.summaryError = this.errorText;
}
@@ -258,7 +277,7 @@ export default {
v-if="helpPopover"
icon="information-o"
:options="helpPopover.options"
- :class="{ 'gl-mr-3': actionButtons.length > 0 }"
+ :class="{ 'gl-mr-3': hasActionButtons }"
>
<template v-if="helpPopover.content">
<p
@@ -275,12 +294,14 @@ export default {
>
</template>
</help-popover>
- <action-buttons
- v-if="actionButtons.length > 0"
- :widget="widgetName"
- :tertiary-buttons="actionButtons"
- @clickedAction="onActionClick"
- />
+ <slot name="action-buttons">
+ <action-buttons
+ v-if="actionButtons.length > 0"
+ :widget="widgetName"
+ :tertiary-buttons="actionButtons"
+ @clickedAction="onActionClick"
+ />
+ </slot>
</div>
<div
v-if="isCollapsible"
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 543136dc659..b64f9c148d1 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
@@ -79,8 +79,11 @@ export default {
</script>
<template>
<div
- class="gl-w-full gl-display-flex gl-align-items-baseline"
- :class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
+ class="gl-w-full gl-display-flex"
+ :class="{
+ 'gl-border-t gl-py-3 gl-pl-7 gl-align-items-baseline': level === 2,
+ 'gl-align-items-center': level === 3,
+ }"
>
<status-icon
v-if="statusIconName && !header"
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
index 03af21a5019..26c986884d3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
@@ -1,4 +1,9 @@
-import { n__, s__, sprintf } from '~/locale';
+import { n__, s__, __, sprintf } from '~/locale';
+
+export const codeQualityPrefixes = {
+ fixed: 'fixed',
+ new: 'new',
+};
export const i18n = {
label: s__('ciReport|Code Quality'),
@@ -7,25 +12,23 @@ export const i18n = {
noChanges: s__(`ciReport|Code Quality hasn't changed.`),
prependText: s__(`ciReport|in`),
fixed: s__(`ciReport|Fixed`),
- pluralReport: (errors) =>
+ findings: (errors, prefix) =>
sprintf(
n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
+ '%{strong_start}%{errors}%{strong_end} %{prefix} finding',
+ '%{strong_start}%{errors}%{strong_end} %{prefix} findings',
errors.length,
),
{
errors: errors.length,
+ prefix,
},
false,
),
- singularReport: (errors) => n__('%d point', '%d points', errors.length),
improvementAndDegradationCopy: (improvement, degradation) =>
- sprintf(
- s__(`ciReport|Code Quality improved on ${improvement} and degraded on ${degradation}.`),
- ),
- improvedCopy: (improvements) =>
- sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)),
- degradedCopy: (degradations) =>
- sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)),
+ sprintf(__('Code Quality scans found %{degradation} and %{improvement}.'), {
+ improvement,
+ degradation,
+ }),
+ singularCopy: (findings) => sprintf(__('Code Quality scans found %{findings}.'), { findings }),
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
index 394f8979a53..4f9bba1e0cb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
@@ -4,7 +4,7 @@ import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/consta
import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { i18n } from './constants';
+import { i18n, codeQualityPrefixes } from './constants';
export default {
name: 'WidgetCodeQuality',
@@ -12,28 +12,36 @@ export default {
props: ['codeQuality', 'blobPath'],
i18n,
computed: {
+ shouldCollapse(data) {
+ const { newErrors, resolvedErrors, parsingInProgress } = data;
+ if (parsingInProgress || (newErrors.length === 0 && resolvedErrors.length === 0)) {
+ return false;
+ }
+ return true;
+ },
summary(data) {
- const { newErrors, resolvedErrors, errorSummary, parsingInProgress } = data;
-
+ const { newErrors, resolvedErrors, parsingInProgress } = data;
if (parsingInProgress) {
return i18n.loading;
- } else if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
+ } else if (newErrors.length >= 1 && resolvedErrors.length >= 1) {
return i18n.improvementAndDegradationCopy(
- i18n.pluralReport(resolvedErrors),
- i18n.pluralReport(newErrors),
+ i18n.findings(resolvedErrors, codeQualityPrefixes.fixed),
+ i18n.findings(newErrors, codeQualityPrefixes.new),
);
- } else if (errorSummary.resolved >= 1) {
- return i18n.improvedCopy(i18n.singularReport(resolvedErrors));
- } else if (errorSummary.errored >= 1) {
- return i18n.degradedCopy(i18n.singularReport(newErrors));
+ } else if (resolvedErrors.length >= 1) {
+ return i18n.singularCopy(i18n.findings(resolvedErrors, codeQualityPrefixes.fixed));
+ } else if (newErrors.length >= 1) {
+ return i18n.singularCopy(i18n.findings(newErrors, codeQualityPrefixes.new));
}
return i18n.noChanges;
},
statusIcon() {
- if (this.collapsedData.errorSummary?.errored >= 1) {
+ if (this.collapsedData.newErrors.length >= 1) {
return EXTENSION_ICONS.warning;
+ } else if (this.collapsedData.resolvedErrors.length >= 1) {
+ return EXTENSION_ICONS.success;
}
- return EXTENSION_ICONS.success;
+ return EXTENSION_ICONS.neutral;
},
},
methods: {
@@ -46,8 +54,6 @@ export default {
parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path),
newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path),
- existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path),
- errorSummary: data.summary,
},
};
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql
new file mode 100644
index 00000000000..c12e4d1febb
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql
@@ -0,0 +1,28 @@
+query securityReportsDownloadPaths(
+ $projectPath: ID!
+ $iid: String!
+ $reportTypes: [SecurityReportTypeEnum!]
+) {
+ project(fullPath: $projectPath) {
+ id
+ mergeRequest(iid: $iid) {
+ id
+ headPipeline {
+ id
+ jobs(securityReportTypes: $reportTypes) {
+ nodes {
+ id
+ name
+ artifacts {
+ # eslint-disable-next-line @graphql-eslint/require-id-when-available
+ nodes {
+ downloadPath
+ fileType
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
new file mode 100644
index 00000000000..f0b20adc5cf
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__, sprintf } from '~/locale';
+import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
+import securityReportMergeRequestDownloadPathsQuery from './graphql/security_report_merge_request_download_paths.query.graphql';
+
+export default {
+ name: 'WidgetSecurityReportsCE',
+ components: {
+ MrWidget,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip,
+ },
+ i18n: {
+ apiError: s__(
+ 'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
+ ),
+ scansHaveRun: s__('SecurityReports|Security scans have run'),
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ hasError: false,
+ };
+ },
+ reportTypes: ['sast', 'secret_detection'],
+ apollo: {
+ reportArtifacts: {
+ query: securityReportMergeRequestDownloadPathsQuery,
+ variables() {
+ return {
+ projectPath: this.mr.targetProjectFullPath,
+ iid: String(this.mr.iid),
+ reportTypes: this.$options.reportTypes.map((r) => r.toUpperCase()),
+ };
+ },
+ update(data) {
+ const artifacts = [];
+
+ (data?.project?.mergeRequest?.headPipeline?.jobs?.nodes || []).forEach((reportType) => {
+ reportType.artifacts?.nodes.forEach((artifact) => {
+ if (artifact.fileType !== 'TRACE') {
+ artifacts.push({
+ name: reportType.name,
+ id: reportType.id,
+ path: artifact.downloadPath,
+ });
+ }
+ });
+ });
+
+ return artifacts;
+ },
+ error() {
+ this.hasError = true;
+ },
+ },
+ },
+ computed: {
+ artifacts() {
+ return this.reportArtifacts || [];
+ },
+ },
+ methods: {
+ handleIsLoading(value) {
+ this.isLoading = value;
+ },
+
+ artifactText({ name }) {
+ return sprintf(s__('SecurityReports|Download %{artifactName}'), {
+ artifactName: name,
+ });
+ },
+ },
+ widgetHelpPopover: {
+ options: { title: s__('ciReport|Security scan results') },
+ content: {
+ text: s__(
+ 'ciReport|New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch.',
+ ),
+ learnMorePath: helpPagePath('user/application_security/index', {
+ anchor: 'view-security-scan-information-in-merge-requests',
+ }),
+ },
+ },
+ icons: EXTENSION_ICONS,
+};
+</script>
+
+<template>
+ <mr-widget
+ :has-error="hasError"
+ :error-text="$options.i18n.apiError"
+ :status-icon-name="$options.icons.warning"
+ :widget-name="$options.name"
+ :is-collapsible="false"
+ :help-popover="$options.widgetHelpPopover"
+ :summary="$options.i18n.scansHaveRun"
+ @is-loading="handleIsLoading"
+ >
+ <template v-if="artifacts.length > 0" #action-buttons>
+ <div class="gl-ml-3">
+ <gl-dropdown
+ v-gl-tooltip
+ icon="download"
+ size="small"
+ category="tertiary"
+ variant="confirm"
+ right
+ >
+ <gl-dropdown-item
+ v-for="artifact in artifacts"
+ :key="artifact.path"
+ :href="artifact.path"
+ :data-testid="`download-${artifact.name}`"
+ download
+ >
+ {{ artifactText(artifact) }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+ </template>
+ </mr-widget>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
index 5fd5950859b..c8d969e3adf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/artifacts_list/actions.js
@@ -1,6 +1,6 @@
import Visibility from 'visibilityjs';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import * as types from './mutation_types';
@@ -63,7 +63,7 @@ export const fetchArtifacts = ({ state, dispatch }) => {
export const receiveArtifactsSuccess = ({ commit }, response) => {
// With 204 we keep polling and don't update the state
- if (response.status === httpStatusCodes.OK) {
+ if (response.status === HTTP_STATUS_OK) {
commit(types.RECEIVE_ARTIFACTS_SUCCESS, response.data);
}
};
diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
index c93057c491c..271cfd210a6 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -66,6 +66,7 @@ export default {
<template>
<gl-link
v-gl-tooltip
+ class="gl-display-inline-flex gl-align-items-center gl-line-height-0 gl-px-3 gl-py-2 gl-rounded-base"
:class="cssClass"
:title="title"
data-qa-selector="status_badge_link"
@@ -75,7 +76,7 @@ export default {
<ci-icon :status="status" :css-classes="iconClasses" />
<template v-if="showText">
- {{ status.text }}
+ <span class="gl-ml-2">{{ status.text }}</span>
</template>
</gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
index 6a03e38a31d..47b96934420 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
@@ -2,6 +2,7 @@
import { s__, sprintf } from '~/locale';
import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
import CiCdAnalyticsAreaChart from './ci_cd_analytics_area_chart.vue';
+import { DEFAULT_SELECTED_CHART } from './constants';
export default {
components: {
@@ -20,7 +21,7 @@ export default {
},
data() {
return {
- selectedChart: 0,
+ selectedChart: DEFAULT_SELECTED_CHART,
};
},
computed: {
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js
index 1561674c0ad..3ac632b4690 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/constants.js
@@ -1 +1,2 @@
export const CHART_CONTAINER_HEIGHT = 300;
+export const DEFAULT_SELECTED_CHART = 2;
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index 8bffc2479a1..0d7547d88a1 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -69,7 +69,7 @@ export default {
computed: {
wrapperStyleClasses() {
const status = this.status.group;
- return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center`;
+ return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status} gl-rounded-full gl-justify-content-center gl-line-height-0`;
},
icon() {
return this.isBorderless ? `${this.status.icon}_borderless` : this.status.icon;
diff --git a/app/assets/javascripts/vue_shared/components/constants.js b/app/assets/javascripts/vue_shared/components/constants.js
new file mode 100644
index 00000000000..b7ff715922d
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/constants.js
@@ -0,0 +1,4 @@
+export const KEY_EDIT = 'edit';
+export const KEY_WEB_IDE = 'webide';
+export const KEY_GITPOD = 'gitpod';
+export const KEY_PIPELINE_EDITOR = 'pipeline_editor';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index 74905dc2ae0..9c30ec67d5a 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -67,6 +67,7 @@ export default {
:suggestions="emojis"
:suggestions-loading="loading"
:get-active-token-value="getActiveEmoji"
+ value-identifier="name"
v-bind="$attrs"
@fetch-suggestions="fetchEmojis"
v-on="$listeners"
diff --git a/app/assets/javascripts/vue_shared/components/group_select/constants.js b/app/assets/javascripts/vue_shared/components/group_select/constants.js
index bc70936eb36..06537d682fe 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/constants.js
+++ b/app/assets/javascripts/vue_shared/components/group_select/constants.js
@@ -1,6 +1,7 @@
import { __ } from '~/locale';
export const TOGGLE_TEXT = __('Search for a group');
+export const RESET_LABEL = __('Reset');
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
index 5db723e1e5a..d295052e2ce 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
@@ -1,26 +1,35 @@
<script>
import { debounce } from 'lodash';
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlFormGroup, GlAlert, GlCollapsibleListbox } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_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,
+ RESET_LABEL,
FETCH_GROUPS_ERROR,
FETCH_GROUP_ERROR,
QUERY_TOO_SHORT_MESSAGE,
} from './constants';
const MINIMUM_QUERY_LENGTH = 3;
+const GROUPS_PER_PAGE = 20;
export default {
components: {
+ GlFormGroup,
+ GlAlert,
GlCollapsibleListbox,
},
props: {
+ label: {
+ type: String,
+ required: true,
+ },
inputName: {
type: String,
required: true,
@@ -54,10 +63,14 @@ export default {
return {
pristine: true,
searching: false,
+ hasMoreGroups: true,
+ infiniteScrollLoading: false,
searchString: '',
groups: [],
+ page: 1,
selectedValue: null,
selectedText: null,
+ errorMessage: '',
};
},
computed: {
@@ -74,6 +87,9 @@ export default {
toggleText() {
return this.selectedText ?? this.$options.i18n.toggleText;
},
+ resetButtonLabel() {
+ return this.clearable ? RESET_LABEL : '';
+ },
inputValue() {
return this.selectedValue ? this.selectedValue : '';
},
@@ -95,35 +111,48 @@ export default {
if (this.isSearchQueryTooShort) {
this.groups = [];
} else {
- this.fetchGroups(searchString);
+ this.fetchGroups();
}
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- async fetchGroups(searchString = '') {
- this.searching = true;
+ async fetchGroups(page = 1) {
+ if (page === 1) {
+ this.searching = true;
+ this.groups = [];
+ this.hasMoreGroups = true;
+ } else {
+ this.infiniteScrollLoading = true;
+ }
try {
- const { data } = await axios.get(
+ const { data, headers } = await axios.get(
Api.buildUrl(groupsPath(this.groupsFilter, this.parentGroupID)),
{
params: {
- search: searchString,
+ search: this.searchString,
+ per_page: GROUPS_PER_PAGE,
+ page,
},
},
);
const groups = data.length ? data : data.results || [];
- this.groups = groups.map((group) => ({
- ...group,
- value: String(group.id),
- }));
+ this.groups.push(
+ ...groups.map((group) => ({
+ ...group,
+ value: String(group.id),
+ })),
+ );
+
+ const { totalPages } = parseIntPagination(normalizeHeaders(headers));
+ if (page === totalPages) {
+ this.hasMoreGroups = false;
+ }
+ this.page = page;
this.searching = false;
+ this.infiniteScrollLoading = false;
} catch (error) {
- createAlert({
- message: FETCH_GROUPS_ERROR,
- error,
- parent: this.$el,
- });
+ this.handleError({ message: FETCH_GROUPS_ERROR, error });
}
},
async fetchInitialSelection() {
@@ -139,11 +168,7 @@ export default {
this.pristine = false;
this.searching = false;
} catch (error) {
- createAlert({
- message: FETCH_GROUP_ERROR,
- error,
- parent: this.$el,
- });
+ this.handleError({ message: FETCH_GROUP_ERROR, error });
}
},
onShown() {
@@ -154,11 +179,20 @@ export default {
onReset() {
this.selected = null;
},
+ onBottomReached() {
+ this.fetchGroups(this.page + 1);
+ },
+ handleError({ message, error }) {
+ Sentry.captureException(error);
+ this.errorMessage = message;
+ },
+ dismissError() {
+ this.errorMessage = '';
+ },
},
i18n: {
toggleText: TOGGLE_TEXT,
selectGroup: __('Select a group'),
- reset: __('Reset'),
noResultsText: __('No results found.'),
searchQueryTooShort: QUERY_TOO_SHORT_MESSAGE,
},
@@ -166,21 +200,27 @@ export default {
</script>
<template>
- <div>
+ <gl-form-group :label="label">
+ <gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" @dismiss="dismissError">{{
+ errorMessage
+ }}</gl-alert>
<gl-collapsible-listbox
ref="listbox"
v-model="selected"
:header-text="$options.i18n.selectGroup"
- :reset-button-label="$options.i18n.reset"
+ :reset-button-label="resetButtonLabel"
:toggle-text="toggleText"
:loading="searching && pristine"
:searching="searching"
:items="groups"
:no-results-text="noResultsText"
+ :infinite-scroll="hasMoreGroups"
+ :infinite-scroll-loading="infiniteScrollLoading"
searchable
@shown="onShown"
@search="search"
@reset="onReset"
+ @bottom-reached="onBottomReached"
>
<template #list-item="{ item }">
<div class="gl-font-weight-bold">
@@ -189,7 +229,6 @@ export default {
<div class="gl-text-gray-300">{{ item.full_path }}</div>
</template>
</gl-collapsible-listbox>
- <div class="flash-container"></div>
<input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
- </div>
+ </gl-form-group>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js b/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js
new file mode 100644
index 00000000000..dbfac8a0339
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/init_group_selects.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import GroupSelect from './group_select.vue';
+
+const SELECTOR = '.js-vue-group-select';
+
+export const initGroupSelects = () => {
+ if (process.env.NODE_ENV !== 'production' && document.querySelector(SELECTOR) === null) {
+ // eslint-disable-next-line no-console
+ console.warn(`Attempted to initialize GroupSelect but '${SELECTOR}' not found in the page`);
+ }
+
+ [...document.querySelectorAll(SELECTOR)].forEach((el) => {
+ const {
+ parentId: parentGroupID,
+ groupsFilter,
+ label,
+ inputName,
+ inputId,
+ selected: initialSelection,
+ testid,
+ } = el.dataset;
+ const clearable = parseBoolean(el.dataset.clearable);
+
+ return new Vue({
+ el,
+ components: {
+ GroupSelect,
+ },
+ render(createElement) {
+ return createElement(GroupSelect, {
+ props: {
+ label,
+ inputName,
+ initialSelection,
+ parentGroupID,
+ groupsFilter,
+ inputId,
+ clearable,
+ },
+ attrs: {
+ 'data-testid': testid,
+ },
+ });
+ },
+ });
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 3c4ae08d2f7..8e459cc21ac 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -75,6 +75,10 @@ export default {
// GraphQL returns `avatarUrl` and Rest `avatar_url`
return this.user?.avatarUrl || this.user?.avatar_url;
},
+ webUrl() {
+ // GraphQL returns `webUrl` and Rest `web_url`
+ return this.user?.webUrl || this.user?.web_url;
+ },
statusTooltipHTML() {
// Rest `status_tooltip_html` which is a ready to work
// html for the emoji and the status text inside a tooltip.
@@ -132,7 +136,7 @@ export default {
:data-user-id="userId"
:data-username="user.username"
:data-name="user.name"
- :href="user.webUrl"
+ :href="webUrl"
target="_blank"
class="js-user-link gl-vertical-align-middle gl-mx-2 gl-align-items-center"
>
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js b/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js
new file mode 100644
index 00000000000..ad89b78b521
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/init_listbox_inputs.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+
+export const initListboxInputs = () => {
+ const els = [...document.querySelectorAll('.js-listbox-input')];
+
+ els.forEach((el, index) => {
+ const { label, description, name, defaultToggleText, value = null } = el.dataset;
+ const { id } = el;
+ const items = JSON.parse(el.dataset.items);
+
+ return new Vue({
+ el,
+ name: `ListboxInputRoot${index + 1}`,
+ data() {
+ return {
+ selected: value,
+ };
+ },
+ render(createElement) {
+ return createElement(ListboxInput, {
+ on: {
+ select: (newValue) => {
+ this.selected = newValue;
+ },
+ },
+ props: {
+ label,
+ description,
+ name,
+ defaultToggleText,
+ selected: this.selected,
+ items,
+ },
+ attrs: {
+ id,
+ },
+ });
+ },
+ });
+ });
+};
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
index b1809e6a9f3..bc6b5d3176f 100644
--- a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
@@ -1,25 +1,37 @@
<script>
-import { GlListbox } from '@gitlab/ui';
+import { GlFormGroup, GlListbox } from '@gitlab/ui';
import { __ } from '~/locale';
-const MIN_ITEMS_COUNT_FOR_SEARCHING = 20;
+const MIN_ITEMS_COUNT_FOR_SEARCHING = 10;
export default {
i18n: {
noResultsText: __('No results found'),
},
components: {
+ GlFormGroup,
GlListbox,
},
model: GlListbox.model,
props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ description: {
+ type: String,
+ required: false,
+ default: '',
+ },
name: {
type: String,
required: true,
},
defaultToggleText: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
selected: {
type: String,
@@ -30,6 +42,11 @@ export default {
type: GlListbox.props.items.type,
required: true,
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -37,6 +54,9 @@ export default {
};
},
computed: {
+ wrapperComponent() {
+ return this.label || this.description ? 'gl-form-group' : 'div';
+ },
allOptions() {
const allOptions = [];
@@ -95,16 +115,17 @@ export default {
</script>
<template>
- <div>
+ <component :is="wrapperComponent" :label="label" :description="description" v-bind="$attrs">
<gl-listbox
:selected="selected"
:toggle-text="toggleText"
:items="filteredItems"
:searchable="isSearchable"
:no-results-text="$options.i18n.noResultsText"
+ :disabled="disabled"
@search="search"
@select="$emit($options.model.event, $event)"
/>
<input ref="input" type="hidden" :name="name" :value="selected" />
- </div>
+ </component>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue
new file mode 100644
index 00000000000..6702a81e747
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/editor_mode_dropdown.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'medium',
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ markdownEditorSelected() {
+ return this.value === 'markdown';
+ },
+ text() {
+ return this.markdownEditorSelected ? __('View rich text') : __('View markdown');
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ category="tertiary"
+ data-qa-selector="editing_mode_switcher"
+ :size="size"
+ :text="text"
+ right
+ >
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="!markdownEditorSelected"
+ @click="$emit('input', 'richText')"
+ ><div class="gl-font-weight-bold">{{ __('Rich text') }}</div>
+ <div class="gl-text-secondary">
+ {{ __('View the formatted output in real-time as you edit.') }}
+ </div>
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="markdownEditorSelected"
+ @click="$emit('input', 'markdown')"
+ ><div class="gl-font-weight-bold">{{ __('Markdown') }}</div>
+ <div class="gl-text-secondary">
+ {{ __('View and edit markdown, with the option to preview the formatted output.') }}
+ </div></gl-dropdown-item
+ >
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index b5f2602af5e..7b76fc3fc6d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -122,6 +122,11 @@ export default {
required: false,
default: () => [],
},
+ showContentEditorSwitcher: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -364,6 +369,8 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
:show-comment-tool-bar="showCommentToolBar"
+ :show-content-editor-switcher="showContentEditorSwitcher"
+ @enableContentEditor="$emit('enableContentEditor')"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 89fffdedbfd..e83441e59a2 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -10,6 +10,7 @@ import {
INDENT_LINE,
OUTDENT_LINE,
} from '~/behaviors/shortcuts/keybindings';
+import { getModifierKey } from '~/constants';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import { CopyAsGFM } from '~/behaviors/markdown/copy_as_gfm';
@@ -66,6 +67,7 @@ export default {
return {
tag: '> ',
suggestPopoverVisible: false,
+ modifierKey: getModifierKey(),
};
},
computed: {
@@ -90,15 +92,6 @@ export default {
const expandText = s__('MarkdownEditor|Click to expand');
return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n');
},
- isMac() {
- // Accessing properties using ?. to allow tests to use
- // this component without setting up window.gl.client.
- // In production, window.gl.client should always be present.
- return Boolean(window.gl?.client?.isMac);
- },
- modifierKey() {
- return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
- },
},
watch: {
showSuggestPopover() {
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 d01eae0308f..c53118b9f62 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -1,16 +1,13 @@
<script>
-import { GlSegmentedControl } from '@gitlab/ui';
-import { __ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import axios from '~/lib/utils/axios_utils';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
import MarkdownField from './field.vue';
export default {
components: {
- MarkdownField,
LocalStorageSync,
- GlSegmentedControl,
+ MarkdownField,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
@@ -91,7 +88,6 @@ export default {
data() {
return {
editingMode: EDITING_MODE_MARKDOWN_FIELD,
- switchEditingControlEnabled: true,
autofocused: false,
};
},
@@ -114,19 +110,16 @@ export default {
updateMarkdownFromMarkdownField({ target }) {
this.$emit('input', target.value);
},
- enableSwitchEditingControl() {
- this.switchEditingControlEnabled = true;
- },
- disableSwitchEditingControl() {
- this.switchEditingControlEnabled = false;
- },
renderMarkdown(markdown) {
return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body);
},
onEditingModeChange(editingMode) {
+ this.editingMode = editingMode;
this.notifyEditingModeChange(editingMode);
},
onEditingModeRestored(editingMode) {
+ this.editingMode = editingMode;
+ this.$emit(editingMode);
this.notifyEditingModeChange(editingMode);
},
notifyEditingModeChange(editingMode) {
@@ -142,25 +135,10 @@ export default {
this.autofocused = true;
},
},
- switchEditingControlOptions: [
- { text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
- { text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR },
- ],
};
</script>
<template>
<div>
- <div class="gl-display-flex gl-justify-content-start gl-mb-3">
- <gl-segmented-control
- v-model="editingMode"
- data-testid="toggle-editing-mode-button"
- data-qa-selector="editing_mode_button"
- class="gl-display-flex"
- :options="$options.switchEditingControlOptions"
- :disabled="!enableContentEditor || !switchEditingControlEnabled"
- @change="onEditingModeChange"
- />
- </div>
<local-storage-sync
v-model="editingMode"
storage-key="gl-wiki-content-editor-enabled"
@@ -176,7 +154,9 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:uploads-path="uploadsPath"
:enable-preview="enablePreview"
+ show-content-editor-switcher
class="bordered-box"
+ @enableContentEditor="onEditingModeChange('contentEditor')"
>
<template #textarea>
<textarea
@@ -205,10 +185,8 @@ export default {
:use-bottom-toolbar="useBottomToolbar"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
- @loading="disableSwitchEditingControl"
- @loadingSuccess="enableSwitchEditingControl"
- @loadingError="enableSwitchEditingControl"
@keydown="$emit('keydown', $event)"
+ @enableMarkdownEditor="onEditingModeChange('markdownField')"
/>
<input
:id="formFieldId"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index b5640e12541..e8be242f660 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlLink, GlLoadingIcon, GlSprintf, GlIcon } from '@gitlab/ui';
+import EditorModeDropdown from './editor_mode_dropdown.vue';
export default {
components: {
@@ -8,6 +9,7 @@ export default {
GlLoadingIcon,
GlSprintf,
GlIcon,
+ EditorModeDropdown,
},
props: {
markdownDocsPath: {
@@ -29,12 +31,24 @@ export default {
required: false,
default: true,
},
+ showContentEditorSwitcher: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
hasQuickActionsDocsPath() {
return this.quickActionsDocsPath !== '';
},
},
+ methods: {
+ handleEditorModeChanged(mode) {
+ if (mode === 'richText') {
+ this.$emit('enableContentEditor');
+ }
+ },
+ },
};
</script>
@@ -121,5 +135,12 @@ export default {
{{ __('Cancel') }}
</gl-button>
</span>
+ <editor-mode-dropdown
+ v-if="showContentEditorSwitcher"
+ size="small"
+ class="gl-float-right gl-line-height-28 gl-display-block"
+ value="markdown"
+ @input="handleEditorModeChanged"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
index 21212e82de4..c83643ca4de 100644
--- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue
@@ -1,6 +1,6 @@
<script>
import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
-import $ from 'jquery';
+import { initScrollingTabs } from '~/layout_nav';
/**
* Given an array of tabs, renders non linked bootstrap tabs.
@@ -41,7 +41,7 @@ export default {
},
},
mounted() {
- $(document).trigger('init.scrolling-tabs');
+ initScrollingTabs();
},
methods: {
shouldRenderBadge(count) {
diff --git a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
index 5f2a66ee0b7..e1f042f78ab 100644
--- a/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination/table_pagination.vue
@@ -64,8 +64,9 @@ export default {
<template>
<gl-pagination
v-if="showPagination"
- class="justify-content-center gl-mt-3"
+ class="gl-mt-3"
v-bind="$attrs"
+ align="center"
:value="pageInfo.page"
:per-page="pageInfo.perPage"
:total-items="pageInfo.total"
diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js
deleted file mode 100644
index 88c975b97b9..00000000000
--- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/constants.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { s__, sprintf } from '~/locale';
-
-export const README_URL =
- 'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/easybuttons.md';
-
-export const CF_BASE_URL =
- 'https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?';
-
-export const TEMPLATES_BASE_URL = 'https://gl-public-templates.s3.amazonaws.com/cfn/experimental/';
-
-export const EASY_BUTTONS = [
- {
- stackName: 'linux-docker-nonspot',
- templateName:
- 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-ondemandonly.cf.yml',
- description: s__(
- 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot.',
- ),
- moreDetails1: s__('Runners|No spot. This is the default choice for Linux Docker executor.'),
- moreDetails2: s__(
- 'Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
- ),
- },
- {
- stackName: 'linux-docker-spotonly',
- templateName: 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-spotonly.cf.yml',
- description: sprintf(
- s__(
- 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. %{percentage} spot.',
- ),
- { percentage: '100%' },
- ),
- moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }),
- moreDetails2: s__(
- 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
- ),
- },
- {
- stackName: 'win2019-shell-non-spot',
- templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-ondemandonly.cf.yml',
- description: s__(
- 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot.',
- ),
- moreDetails1: s__('Runners|No spot. Default choice for Windows Shell executor.'),
- moreDetails2: s__(
- 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
- ),
- },
- {
- stackName: 'win2019-shell-spot',
- templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-spotonly.cf.yml',
- description: sprintf(
- s__(
- 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. %{percentage} spot.',
- ),
- { percentage: '100%' },
- ),
- moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }),
- moreDetails2: s__(
- 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
- ),
- },
-];
diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
index eee65d90285..08acde1aefc 100644
--- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
@@ -1,125 +1,29 @@
<script>
-import {
- GlModal,
- GlSprintf,
- GlLink,
- GlFormRadioGroup,
- GlFormRadio,
- GlAccordion,
- GlAccordionItem,
-} from '@gitlab/ui';
-import Tracking from '~/tracking';
-import { getBaseURL, objectToQuery, visitUrl } from '~/lib/utils/url_utility';
-import { __, s__ } from '~/locale';
-import { README_URL, CF_BASE_URL, TEMPLATES_BASE_URL, EASY_BUTTONS } from './constants';
+import { GlModal } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
export default {
components: {
GlModal,
- GlSprintf,
- GlLink,
- GlFormRadioGroup,
- GlFormRadio,
- GlAccordion,
- GlAccordionItem,
+ RunnerAwsInstructions,
},
- mixins: [Tracking.mixin()],
props: {
modalId: {
type: String,
required: true,
},
},
- data() {
- return {
- selected: this.$options.easyButtons[0],
- };
- },
methods: {
- borderBottom(idx) {
- return idx < this.$options.easyButtons.length - 1;
- },
- easyButtonUrl(easyButton) {
- const params = {
- templateURL: TEMPLATES_BASE_URL + easyButton.templateName,
- stackName: easyButton.stackName,
- param_3GITLABRunnerInstanceURL: getBaseURL(),
- };
- return CF_BASE_URL + objectToQuery(params);
- },
- trackCiRunnerTemplatesClick(stackName) {
- this.track('template_clicked', {
- label: stackName,
- });
- },
- handleModalPrimary() {
- this.trackCiRunnerTemplatesClick(this.selected.stackName);
- visitUrl(this.easyButtonUrl(this.selected), true);
+ onClose() {
+ this.$refs.modal.close();
},
},
- i18n: {
- title: s__('Runners|Deploy GitLab Runner in AWS'),
- instructions: s__(
- 'Runners|Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console.',
- ),
- chooseRunner: s__('Runners|Choose your preferred GitLab Runner'),
- dontSeeWhatYouAreLookingFor: s__(
- "Runners|Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}.",
- ),
- moreDetails: __('More Details'),
- lessDetails: __('Less Details'),
- },
- deployButton: {
- text: s__('Runners|Deploy GitLab Runner in AWS'),
- attributes: [{ variant: 'confirm' }],
- },
- closeButton: {
- text: __('Cancel'),
- attributes: [{ variant: 'default' }],
- },
- readmeUrl: README_URL,
- easyButtons: EASY_BUTTONS,
+ i18n_title: s__('Runners|Deploy GitLab Runner in AWS'),
};
</script>
<template>
- <gl-modal
- :modal-id="modalId"
- :title="$options.i18n.title"
- :action-primary="$options.deployButton"
- :action-secondary="$options.closeButton"
- size="sm"
- @primary="handleModalPrimary"
- >
- <p>{{ $options.i18n.instructions }}</p>
- <gl-form-radio-group v-model="selected" :label="$options.i18n.chooseRunner" label-sr-only>
- <gl-form-radio
- v-for="(easyButton, idx) in $options.easyButtons"
- :key="easyButton.templateName"
- :value="easyButton"
- class="gl-py-5 gl-pl-8"
- :class="{ 'gl-border-b': borderBottom(idx) }"
- >
- <div class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold">
- {{ easyButton.description }}
- <gl-accordion :header-level="3" class="gl-pt-3">
- <gl-accordion-item
- :title="$options.i18n.moreDetails"
- :title-visible="$options.i18n.lessDetails"
- class="gl-font-weight-normal"
- >
- <p class="gl-pt-2">{{ easyButton.moreDetails1 }}</p>
- <p class="gl-m-0">{{ easyButton.moreDetails2 }}</p>
- </gl-accordion-item>
- </gl-accordion>
- </div>
- </gl-form-radio>
- </gl-form-radio-group>
- <p>
- <gl-sprintf :message="$options.i18n.dontSeeWhatYouAreLookingFor">
- <template #link="{ content }">
- <gl-link :href="$options.readmeUrl" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
+ <gl-modal ref="modal" :modal-id="modalId" :title="$options.i18n_title" hide-footer size="sm">
+ <runner-aws-instructions @close="onClose" />
</gl-modal>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
index c97e191b630..3dbc5246c3d 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/constants.js
@@ -1,18 +1,69 @@
-import { s__ } from '~/locale';
+import { s__, sprintf } from '~/locale';
export const REGISTRATION_TOKEN_PLACEHOLDER = '$REGISTRATION_TOKEN';
-export const INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES = {
- docker: {
- instructions: s__(
- 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
+export const PLATFORM_DOCKER = 'docker';
+export const PLATFORM_KUBERNETES = 'kubernetes';
+
+export const AWS_README_URL =
+ 'https://gitlab.com/guided-explorations/aws/gitlab-runner-autoscaling-aws-asg/-/blob/main/easybuttons.md';
+
+export const AWS_CF_BASE_URL =
+ 'https://us-west-2.console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/create/review?';
+
+export const AWS_TEMPLATES_BASE_URL =
+ 'https://gl-public-templates.s3.amazonaws.com/cfn/experimental/';
+
+export const AWS_EASY_BUTTONS = [
+ {
+ stackName: 'linux-docker-nonspot',
+ templateName:
+ 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-ondemandonly.cf.yml',
+ description: s__(
+ 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot.',
+ ),
+ moreDetails1: s__('Runners|No spot. This is the default choice for Linux Docker executor.'),
+ moreDetails2: s__(
+ 'Runners|A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
+ ),
+ },
+ {
+ stackName: 'linux-docker-spotonly',
+ templateName: 'easybutton-amazon-linux-2-docker-manual-scaling-with-schedule-spotonly.cf.yml',
+ description: sprintf(
+ s__(
+ 'Runners|Amazon Linux 2 Docker HA with manual scaling and optional scheduling. %{percentage} spot.',
+ ),
+ { percentage: '100%' },
+ ),
+ moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }),
+ moreDetails2: s__(
+ 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
+ ),
+ },
+ {
+ stackName: 'win2019-shell-non-spot',
+ templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-ondemandonly.cf.yml',
+ description: s__(
+ 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. Non-spot.',
+ ),
+ moreDetails1: s__('Runners|No spot. Default choice for Windows Shell executor.'),
+ moreDetails2: s__(
+ 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
),
- link: 'https://docs.gitlab.com/runner/install/docker.html',
},
- kubernetes: {
- instructions: s__(
- 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
+ {
+ stackName: 'win2019-shell-spot',
+ templateName: 'easybutton-windows2019-shell-manual-scaling-with-scheduling-spotonly.cf.yml',
+ description: sprintf(
+ s__(
+ 'Runners|Windows 2019 Shell with manual scaling and optional scheduling. %{percentage} spot.',
+ ),
+ { percentage: '100%' },
+ ),
+ moreDetails1: sprintf(s__('Runners|%{percentage} spot.'), { percentage: '100%' }),
+ moreDetails2: s__(
+ 'Runners|Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.',
),
- link: 'https://docs.gitlab.com/runner/install/kubernetes.html',
},
-};
+];
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql
index 76f152e5453..76f152e5453 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql
index c0248a35e3f..c0248a35e3f 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
new file mode 100644
index 00000000000..cafebdfe5f4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue
@@ -0,0 +1,123 @@
+<script>
+import {
+ GlButton,
+ GlSprintf,
+ GlLink,
+ GlFormRadioGroup,
+ GlFormRadio,
+ GlAccordion,
+ GlAccordionItem,
+} from '@gitlab/ui';
+import Tracking from '~/tracking';
+import { getBaseURL, objectToQuery, visitUrl } from '~/lib/utils/url_utility';
+import { __, s__ } from '~/locale';
+import {
+ AWS_README_URL,
+ AWS_CF_BASE_URL,
+ AWS_TEMPLATES_BASE_URL,
+ AWS_EASY_BUTTONS,
+} from '../constants';
+
+export default {
+ components: {
+ GlButton,
+ GlSprintf,
+ GlLink,
+ GlFormRadioGroup,
+ GlFormRadio,
+ GlAccordion,
+ GlAccordionItem,
+ },
+ mixins: [Tracking.mixin()],
+ data() {
+ return {
+ selectedIndex: 0,
+ };
+ },
+ computed: {
+ selected() {
+ return this.$options.easyButtons[this.selectedIndex];
+ },
+ },
+ methods: {
+ borderBottom(idx) {
+ return idx < this.$options.easyButtons.length - 1;
+ },
+ easyButtonUrl(easyButton) {
+ const params = {
+ templateURL: AWS_TEMPLATES_BASE_URL + easyButton.templateName,
+ stackName: easyButton.stackName,
+ param_3GITLABRunnerInstanceURL: getBaseURL(),
+ };
+ return AWS_CF_BASE_URL + objectToQuery(params);
+ },
+ trackCiRunnerTemplatesClick(stackName) {
+ this.track('template_clicked', {
+ label: stackName,
+ });
+ },
+ onOk() {
+ this.trackCiRunnerTemplatesClick(this.selected.stackName);
+ visitUrl(this.easyButtonUrl(this.selected), true);
+ },
+ onClose() {
+ this.$emit('close');
+ },
+ },
+ i18n: {
+ title: s__('Runners|Deploy GitLab Runner in AWS'),
+ instructions: s__(
+ 'Runners|Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console.',
+ ),
+ chooseRunner: s__('Runners|Choose your preferred GitLab Runner'),
+ dontSeeWhatYouAreLookingFor: s__(
+ "Runners|Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}.",
+ ),
+ moreDetails: __('More Details'),
+ lessDetails: __('Less Details'),
+ },
+ readmeUrl: AWS_README_URL,
+ easyButtons: AWS_EASY_BUTTONS,
+};
+</script>
+<template>
+ <div>
+ <p>{{ $options.i18n.instructions }}</p>
+ <gl-form-radio-group v-model="selectedIndex" :label="$options.i18n.chooseRunner" label-sr-only>
+ <gl-form-radio
+ v-for="(easyButton, idx) in $options.easyButtons"
+ :key="easyButton.templateName"
+ :value="idx"
+ class="gl-py-5 gl-pl-8"
+ :class="{ 'gl-border-b': borderBottom(idx) }"
+ >
+ <div class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold">
+ {{ easyButton.description }}
+ <gl-accordion :header-level="3" class="gl-pt-3">
+ <gl-accordion-item
+ :title="$options.i18n.moreDetails"
+ :title-visible="$options.i18n.lessDetails"
+ class="gl-font-weight-normal"
+ >
+ <p class="gl-pt-2">{{ easyButton.moreDetails1 }}</p>
+ <p class="gl-m-0">{{ easyButton.moreDetails2 }}</p>
+ </gl-accordion-item>
+ </gl-accordion>
+ </div>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ <p>
+ <gl-sprintf :message="$options.i18n.dontSeeWhatYouAreLookingFor">
+ <template #link="{ content }">
+ <gl-link :href="$options.readmeUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <footer class="gl-display-flex gl-justify-content-end gl-pt-3 gl-gap-3">
+ <gl-button @click="onClose()">{{ __('Close') }}</gl-button>
+ <gl-button variant="confirm" @click="onOk()">
+ {{ s__('Runners|Deploy GitLab Runner in AWS') }}
+ </gl-button>
+ </footer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
new file mode 100644
index 00000000000..36e608a068b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue
@@ -0,0 +1,169 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { REGISTRATION_TOKEN_PLACEHOLDER } from '../constants';
+import getRunnerSetupInstructionsQuery from '../graphql/get_runner_setup.query.graphql';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlLoadingIcon,
+ ModalCopyButton,
+ },
+ props: {
+ platform: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ registrationToken: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ selectedArchitecture: this.platform?.architectures[0] || null,
+ instructions: null,
+ };
+ },
+ apollo: {
+ instructions: {
+ query: getRunnerSetupInstructionsQuery,
+ skip() {
+ return !this.platform || !this.selectedArchitecture;
+ },
+ variables() {
+ return {
+ platform: this.platform.name,
+ architecture: this.selectedArchitecture.name,
+ };
+ },
+ update(data) {
+ return data?.runnerSetup;
+ },
+ error() {
+ this.$emit('error');
+ },
+ },
+ },
+ computed: {
+ architectures() {
+ return this.platform?.architectures || [];
+ },
+ binaryUrl() {
+ return this.selectedArchitecture?.downloadLocation;
+ },
+ registerInstructionsWithToken() {
+ const { registerInstructions } = this.instructions || {};
+
+ if (this.registrationToken) {
+ return registerInstructions?.replace(
+ REGISTRATION_TOKEN_PLACEHOLDER,
+ this.registrationToken,
+ );
+ }
+ return registerInstructions;
+ },
+ },
+ watch: {
+ platform() {
+ // reset selection if architecture is not in this list
+ const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture.name);
+ if (!arch) {
+ this.selectArchitecture(this.architectures[0]);
+ }
+ },
+ },
+ methods: {
+ selectArchitecture(architecture) {
+ this.selectedArchitecture = architecture;
+ },
+ onClose() {
+ this.$emit('close');
+ },
+ },
+ i18n: {
+ architecture: s__('Runners|Architecture'),
+ downloadInstallBinary: s__('Runners|Download and install binary'),
+ downloadLatestBinary: s__('Runners|Download latest binary'),
+ registerRunnerCommand: s__('Runners|Command to register runner'),
+ copyInstructions: s__('Runners|Copy instructions'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <h5>
+ {{ $options.i18n.architecture }}
+ <gl-loading-icon v-if="$apollo.loading" size="sm" inline />
+ </h5>
+
+ <gl-dropdown class="gl-mb-3" :text="selectedArchitecture.name">
+ <gl-dropdown-item
+ v-for="architecture in architectures"
+ :key="architecture.name"
+ is-check-item
+ :is-checked="selectedArchitecture.name === architecture.name"
+ data-testid="architecture-dropdown-item"
+ @click="selectArchitecture(architecture)"
+ >
+ {{ architecture.name }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
+ <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
+ <gl-button
+ v-if="binaryUrl"
+ class="gl-ml-auto"
+ :href="binaryUrl"
+ download
+ icon="download"
+ data-testid="binary-download-button"
+ >
+ {{ $options.i18n.downloadLatestBinary }}
+ </gl-button>
+ </div>
+
+ <template v-if="instructions">
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
+ data-testid="binary-instructions"
+ >{{ instructions.installInstructions }}</pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="instructions.installInstructions"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ <h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5>
+ <div class="gl-display-flex">
+ <pre
+ class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
+ data-testid="register-command"
+ >{{ registerInstructionsWithToken }}</pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyInstructions"
+ :text="registerInstructionsWithToken"
+ :modal-id="$options.modalId"
+ css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
+ category="tertiary"
+ />
+ </div>
+ </template>
+
+ <footer class="gl-display-flex gl-justify-content-end gl-pt-3">
+ <gl-button @click="onClose()">{{ __('Close') }}</gl-button>
+ </footer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue
new file mode 100644
index 00000000000..ff7e803af2a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ },
+ methods: {
+ onClose() {
+ this.$emit('close');
+ },
+ },
+ I18N_INSTRUCTIONS_TEXT: s__(
+ 'Runners|To install Runner in a container follow the instructions described in the GitLab documentation',
+ ),
+ I18N_VIEW_INSTRUCTIONS: s__('Runners|View installation instructions'),
+ HELP_URL: 'https://docs.gitlab.com/runner/install/docker.html',
+};
+</script>
+<template>
+ <div>
+ <p>
+ {{ $options.I18N_INSTRUCTIONS_TEXT }}
+ </p>
+ <gl-button :href="$options.HELP_URL">
+ <gl-icon name="external-link" />
+ {{ $options.I18N_VIEW_INSTRUCTIONS }}
+ </gl-button>
+ <footer class="gl-display-flex gl-justify-content-end gl-pt-3">
+ <gl-button @click="onClose()">{{ __('Close') }}</gl-button>
+ </footer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue
new file mode 100644
index 00000000000..ee41dab0cec
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ },
+ methods: {
+ onClose() {
+ this.$emit('close');
+ },
+ },
+ I18N_INSTRUCTIONS_TEXT: s__(
+ 'Runners|To install Runner in Kubernetes follow the instructions described in the GitLab documentation.',
+ ),
+ I18N_VIEW_INSTRUCTIONS: s__('Runners|View installation instructions'),
+ HELP_URL: 'https://docs.gitlab.com/runner/install/kubernetes.html',
+};
+</script>
+<template>
+ <div>
+ <p>
+ {{ $options.I18N_INSTRUCTIONS_TEXT }}
+ </p>
+ <gl-button :href="$options.HELP_URL">
+ <gl-icon name="external-link" />
+ {{ $options.I18N_VIEW_INSTRUCTIONS }}
+ </gl-button>
+ <footer class="gl-display-flex gl-justify-content-end gl-pt-3">
+ <gl-button @click="onClose()">{{ __('Close') }}</gl-button>
+ </footer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index c5d3704ead9..729fe9c462c 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -12,15 +12,13 @@ import {
GlResizeObserverDirective,
} from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { isEmpty } from 'lodash';
import { __, s__ } from '~/locale';
-import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
-import {
- INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES,
- REGISTRATION_TOKEN_PLACEHOLDER,
-} from './constants';
-import getRunnerPlatformsQuery from './graphql/queries/get_runner_platforms.query.graphql';
-import getRunnerSetupInstructionsQuery from './graphql/queries/get_runner_setup.query.graphql';
+import getRunnerPlatformsQuery from './graphql/get_runner_platforms.query.graphql';
+import { PLATFORM_DOCKER, PLATFORM_KUBERNETES } from './constants';
+
+import RunnerCliInstructions from './instructions/runner_cli_instructions.vue';
+import RunnerDockerInstructions from './instructions/runner_docker_instructions.vue';
+import RunnerKubernetesInstructions from './instructions/runner_kubernetes_instructions.vue';
export default {
components: {
@@ -33,7 +31,7 @@ export default {
GlIcon,
GlLoadingIcon,
GlSkeletonLoader,
- ModalCopyButton,
+ RunnerDockerInstructions,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -74,27 +72,13 @@ export default {
);
},
result() {
- // If it is set and available, select the defaultSelectedPlatform.
+ // If found, select the defaultSelectedPlatform.
// Otherwise, select the first available platform
- this.selectPlatform(this.defaultPlatformName || this.platforms?.[0].name);
- },
- error() {
- this.toggleAlert(true);
- },
- },
- instructions: {
- query: getRunnerSetupInstructionsQuery,
- skip() {
- return !this.shown || !this.selectedPlatform;
- },
- variables() {
- return {
- platform: this.selectedPlatform,
- architecture: this.selectedArchitecture || '',
- };
- },
- update(data) {
- return data?.runnerSetup;
+ const platform =
+ this.platforms?.find(({ name }) => this.defaultPlatformName === name) ||
+ this.platforms?.[0];
+
+ this.selectPlatform(platform);
},
error() {
this.toggleAlert(true);
@@ -106,39 +90,23 @@ export default {
shown: false,
platforms: [],
selectedPlatform: null,
- selectedArchitecture: null,
showAlert: false,
- instructions: {},
platformsButtonGroupVertical: false,
};
},
computed: {
- instructionsEmpty() {
- return isEmpty(this.instructions);
- },
- architectures() {
- return this.platforms.find(({ name }) => name === this.selectedPlatform)?.architectures || [];
- },
- binaryUrl() {
- return this.architectures.find(({ name }) => name === this.selectedArchitecture)
- ?.downloadLocation;
- },
- instructionsWithoutArchitecture() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.instructions;
- },
- runnerInstallationLink() {
- return INSTRUCTIONS_PLATFORMS_WITHOUT_ARCHITECTURES[this.selectedPlatform]?.link;
- },
- registerInstructionsWithToken() {
- const { registerInstructions } = this.instructions || {};
-
- if (this.registrationToken) {
- return registerInstructions?.replace(
- REGISTRATION_TOKEN_PLACEHOLDER,
- this.registrationToken,
- );
+ instructionsComponent() {
+ if (this.selectedPlatform?.architectures?.length) {
+ return RunnerCliInstructions;
+ }
+ switch (this.selectedPlatform?.name) {
+ case PLATFORM_DOCKER:
+ return RunnerDockerInstructions;
+ case PLATFORM_KUBERNETES:
+ return RunnerKubernetesInstructions;
+ default:
+ return null;
}
- return registerInstructions;
},
},
updated() {
@@ -149,6 +117,12 @@ export default {
show() {
this.$refs.modal.show();
},
+ close() {
+ this.$refs.modal.close();
+ },
+ onClose() {
+ this.close();
+ },
onShown() {
this.shown = true;
this.refocusSelectedPlatformButton();
@@ -159,21 +133,13 @@ export default {
// get focused when setting a `defaultPlatformName`.
// This method refocuses the expected button.
// See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open
- this.$refs[this.selectedPlatform]?.[0].$el.focus();
+ this.$refs[this.selectedPlatform?.name]?.[0].$el.focus();
},
- selectPlatform(platformName) {
- this.selectedPlatform = platformName;
-
- // Update architecture when platform changes
- const arch = this.architectures.find(({ name }) => name === this.selectedArchitecture);
- if (arch) {
- this.selectArchitecture(arch.name);
- } else {
- this.selectArchitecture(this.architectures[0]?.name);
- }
+ selectPlatform(platform) {
+ this.selectedPlatform = platform;
},
- selectArchitecture(architecture) {
- this.selectedArchitecture = architecture;
+ isPlatformSelected(platform) {
+ return this.selectedPlatform.name === platform.name;
},
toggleAlert(state) {
this.showAlert = state;
@@ -189,17 +155,9 @@ export default {
i18n: {
environment: __('Environment'),
installARunner: s__('Runners|Install a runner'),
- architecture: s__('Runners|Architecture'),
downloadInstallBinary: s__('Runners|Download and install binary'),
downloadLatestBinary: s__('Runners|Download latest binary'),
- registerRunnerCommand: s__('Runners|Command to register runner'),
fetchError: s__('Runners|An error has occurred fetching instructions'),
- copyInstructions: s__('Runners|Copy instructions'),
- viewInstallationInstructions: s__('Runners|View installation instructions'),
- },
- closeButton: {
- text: __('Close'),
- attributes: [{ variant: 'default' }],
},
};
</script>
@@ -208,8 +166,8 @@ export default {
ref="modal"
:modal-id="modalId"
:title="$options.i18n.installARunner"
- :action-secondary="$options.closeButton"
v-bind="$attrs"
+ hide-footer
v-on="$listeners"
@shown="onShown"
>
@@ -234,88 +192,23 @@ export default {
v-for="platform in platforms"
:key="platform.name"
:ref="platform.name"
- :selected="selectedPlatform === platform.name"
- @click="selectPlatform(platform.name)"
+ :selected="isPlatformSelected(platform)"
+ @click="selectPlatform(platform)"
>
{{ platform.humanReadableName }}
</gl-button>
</gl-button-group>
</div>
</template>
- <template v-if="architectures.length">
- <template v-if="selectedPlatform">
- <h5>
- {{ $options.i18n.architecture }}
- <gl-loading-icon v-if="$apollo.loading" size="sm" inline />
- </h5>
-
- <gl-dropdown class="gl-mb-3" :text="selectedArchitecture">
- <gl-dropdown-item
- v-for="architecture in architectures"
- :key="architecture.name"
- is-check-item
- :is-checked="selectedArchitecture === architecture.name"
- data-testid="architecture-dropdown-item"
- @click="selectArchitecture(architecture.name)"
- >
- {{ architecture.name }}
- </gl-dropdown-item>
- </gl-dropdown>
- <div class="gl-sm-display-flex gl-align-items-center gl-mb-3">
- <h5>{{ $options.i18n.downloadInstallBinary }}</h5>
- <gl-button
- v-if="binaryUrl"
- class="gl-ml-auto"
- :href="binaryUrl"
- download
- icon="download"
- data-testid="binary-download-button"
- >
- {{ $options.i18n.downloadLatestBinary }}
- </gl-button>
- </div>
- </template>
- <template v-if="!instructionsEmpty">
- <div class="gl-display-flex">
- <pre
- class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
- data-testid="binary-instructions"
- >{{ instructions.installInstructions }}</pre
- >
- <modal-copy-button
- :title="$options.i18n.copyInstructions"
- :text="instructions.installInstructions"
- :modal-id="$options.modalId"
- css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
- category="tertiary"
- />
- </div>
- <h5 class="gl-mb-3">{{ $options.i18n.registerRunnerCommand }}</h5>
- <div class="gl-display-flex">
- <pre
- class="gl-bg-gray gl-flex-grow-1 gl-white-space-pre-line"
- data-testid="register-command"
- >{{ registerInstructionsWithToken }}</pre
- >
- <modal-copy-button
- :title="$options.i18n.copyInstructions"
- :text="registerInstructionsWithToken"
- :modal-id="$options.modalId"
- css-classes="gl-align-self-start gl-ml-2 gl-mt-2"
- category="tertiary"
- />
- </div>
- </template>
- </template>
- <template v-else>
- <div>
- <p>{{ instructionsWithoutArchitecture }}</p>
- <gl-button :href="runnerInstallationLink">
- <gl-icon name="external-link" />
- {{ $options.i18n.viewInstallationInstructions }}
- </gl-button>
- </div>
- </template>
+ <keep-alive>
+ <component
+ :is="instructionsComponent"
+ :registration-token="registrationToken"
+ :platform="selectedPlatform"
+ @close="onClose"
+ @error="toggleAlert(true)"
+ />
+ </keep-alive>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index a28460dd58e..f382ded90d7 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -140,3 +140,7 @@ export const BIDI_CHARS_CLASS_LIST = 'unicode-bidi has-tooltip';
export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
+
+// We fallback to highlighting these languages with Rouge, see the following issue for more detail:
+// https://gitlab.com/gitlab-org/gitlab/-/issues/384375#note_1212752013
+export const LEGACY_FALLBACKS = ['python'];
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 0cfee93ce5d..efafa67a733 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
@@ -11,6 +11,7 @@ import {
EVENT_LABEL_FALLBACK,
ROUGE_TO_HLJS_LANGUAGE_MAP,
LINES_PER_CHUNK,
+ LEGACY_FALLBACKS,
} from './constants';
import Chunk from './components/chunk.vue';
import { registerPlugins } from './plugins/index';
@@ -57,10 +58,11 @@ export default {
},
unsupportedLanguage() {
const supportedLanguages = Object.keys(languageLoader);
- return (
+ const unsupportedLanguage =
!supportedLanguages.includes(this.language) &&
- !supportedLanguages.includes(this.blob.language?.toLowerCase())
- );
+ !supportedLanguages.includes(this.blob.language?.toLowerCase());
+
+ return LEGACY_FALLBACKS.includes(this.language) || unsupportedLanguage;
},
totalChunks() {
return Object.keys(this.chunks).length;
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
index 423501265d7..247f49c1345 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
@@ -30,6 +30,11 @@ export default {
required: true,
default: () => [],
},
+ additionalClass: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -96,7 +101,14 @@ export default {
:value="timezoneIdentifier || value"
type="hidden"
/>
- <gl-dropdown :text="selectedTimezoneLabel" block lazy menu-class="gl-w-full!" v-bind="$attrs">
+ <gl-dropdown
+ :text="selectedTimezoneLabel"
+ :class="additionalClass"
+ block
+ lazy
+ menu-class="gl-w-full!"
+ v-bind="$attrs"
+ >
<gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus />
<gl-dropdown-item
v-for="timezone in filteredResults"
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 383dc27ea5e..98630512308 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,16 +1,13 @@
<script>
import { GlModal, GlSprintf, GlLink, GlPopover } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import { visitUrl } from '~/lib/utils/url_utility';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-export const KEY_EDIT = 'edit';
-export const KEY_WEB_IDE = 'webide';
-export const KEY_GITPOD = 'gitpod';
-export const KEY_PIPELINE_EDITOR = 'pipeline_editor';
+import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
export const i18n = {
modal: {
@@ -221,7 +218,13 @@ export default {
this.showModal('showForkModal');
},
}
- : { href: this.webIdeUrl };
+ : {
+ href: this.webIdeUrl,
+ handle: (evt) => {
+ evt.preventDefault();
+ visitUrl(this.webIdeUrl, true);
+ },
+ };
return {
key: KEY_WEB_IDE,
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index 2f85a29fb84..c93dd95a886 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -9,7 +9,7 @@ const INTERVALS = {
export const FILE_SYMLINK_MODE = '120000';
-export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
+export const SHORT_DATE_FORMAT = 'mmm dd, yyyy';
export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
index 2fc1f935501..387fc5e0d1c 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
@@ -1,6 +1,5 @@
<script>
import { GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
-import $ from 'jquery';
import Autosave from '~/autosave';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
@@ -81,13 +80,13 @@ export default {
if (!titleInput || !descriptionInput) return;
- this.autosaveTitle = new Autosave($(titleInput.$el), [
+ this.autosaveTitle = new Autosave(titleInput.$el, [
document.location.pathname,
document.location.search,
'title',
]);
- this.autosaveDescription = new Autosave($(descriptionInput.$el), [
+ this.autosaveDescription = new Autosave(descriptionInput, [
document.location.pathname,
document.location.search,
'description',
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
index b6a459f21e0..26309a25f07 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -34,7 +34,7 @@ export default {
:href="`#${panel.name}`"
data-qa-selector="panel_link"
:data-qa-panel-name="panel.name"
- class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!"
+ class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-3 gl-hover-text-decoration-none!"
@click="track('click_tab', { label: panel.name })"
>
<div
diff --git a/app/assets/javascripts/work_items/components/notes/activity_filter.vue b/app/assets/javascripts/work_items/components/notes/activity_filter.vue
new file mode 100644
index 00000000000..71784d3a807
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/activity_filter.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { ASC, DESC } from '~/notes/constants';
+import { TRACKING_CATEGORY_SHOW, WORK_ITEM_NOTES_SORT_ORDER_KEY } from '~/work_items/constants';
+
+const SORT_OPTIONS = [
+ { key: DESC, text: __('Newest first'), dataid: 'js-newest-first' },
+ { key: ASC, text: __('Oldest first'), dataid: 'js-oldest-first' },
+];
+
+export default {
+ SORT_OPTIONS,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ LocalStorageSync,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ sortOrder: {
+ type: String,
+ default: ASC,
+ required: false,
+ },
+ loading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ persistSortOrder: true,
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_track_notes_sorting',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ selectedSortOption() {
+ const isSortOptionValid = this.sortOrder === ASC || this.sortOrder === DESC;
+ return isSortOptionValid ? SORT_OPTIONS.find(({ key }) => this.sortOrder === key) : ASC;
+ },
+ getDropdownSelectedText() {
+ return this.selectedSortOption.text;
+ },
+ },
+ methods: {
+ setDiscussionSortDirection(direction) {
+ this.$emit('updateSavedSortOrder', direction);
+ },
+ fetchSortedDiscussions(direction) {
+ if (this.isSortDropdownItemActive(direction)) {
+ return;
+ }
+ this.track('notes_sort_order_changed');
+ this.$emit('changeSortOrder', direction);
+ },
+ isSortDropdownItemActive(sortDir) {
+ return sortDir === this.sortOrder;
+ },
+ },
+ WORK_ITEM_NOTES_SORT_ORDER_KEY,
+};
+</script>
+
+<template>
+ <div
+ id="discussion-preferences"
+ data-testid="discussion-preferences"
+ class="gl-display-inline-block gl-vertical-align-bottom gl-w-full gl-sm-w-auto"
+ >
+ <local-storage-sync
+ :value="sortOrder"
+ :storage-key="$options.WORK_ITEM_NOTES_SORT_ORDER_KEY"
+ :persist="persistSortOrder"
+ as-string
+ @input="setDiscussionSortDirection"
+ />
+ <gl-dropdown
+ :id="`discussion-preferences-dropdown-${workItemType}`"
+ class="gl-xs-w-full"
+ size="small"
+ :text="getDropdownSelectedText"
+ :disabled="loading"
+ right
+ >
+ <div id="discussion-sort">
+ <gl-dropdown-item
+ v-for="{ text, key, dataid } in $options.SORT_OPTIONS"
+ :key="text"
+ :data-testid="dataid"
+ is-check-item
+ :is-checked="isSortDropdownItemActive(key)"
+ @click="fetchSortedDiscussions(key)"
+ >
+ {{ text }}
+ </gl-dropdown-item>
+ </div>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
new file mode 100644
index 00000000000..5efa9c94f2b
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+
+export default {
+ components: {
+ NoteHeader,
+ NoteBody,
+ TimelineEntryItem,
+ GlAvatarLink,
+ GlAvatar,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ author() {
+ return this.note.author;
+ },
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <timeline-entry-item
+ :id="noteAnchorId"
+ :class="{ 'internal-note': note.internal }"
+ :data-note-id="note.id"
+ class="note note-wrapper note-comment"
+ >
+ <div class="timeline-avatar gl-float-left">
+ <gl-avatar-link :href="author.webUrl">
+ <gl-avatar
+ :src="author.avatarUrl"
+ :entity-name="author.username"
+ :alt="author.name"
+ :size="32"
+ />
+ </gl-avatar-link>
+ </div>
+
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header :author="author" :created-at="note.createdAt" :note-id="note.id" />
+ </div>
+ <div class="timeline-discussion-body">
+ <note-body :note="note" />
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue
new file mode 100644
index 00000000000..dcee8750f81
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_body.vue
@@ -0,0 +1,37 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+export default {
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ mounted() {
+ this.renderGFM();
+ },
+ methods: {
+ renderGFM() {
+ renderGFM(this.$refs['note-body']);
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
+ },
+};
+</script>
+
+<template>
+ <div ref="note-body" class="note-body">
+ <div
+ v-safe-html:[$options.safeHtmlConfig]="note.bodyHtml"
+ class="note-text md"
+ data-testid="work-item-note-body"
+ ></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/work_item_comment_form.vue
new file mode 100644
index 00000000000..65042f1431d
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_comment_form.vue
@@ -0,0 +1,228 @@
+<script>
+import { GlAvatar, GlButton } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import { __, s__ } from '~/locale';
+import Tracking from '~/tracking';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { getWorkItemQuery, getWorkItemNotesQuery } from '../utils';
+import createNoteMutation from '../graphql/create_work_item_note.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW } from '../constants';
+import WorkItemNoteSignedOut from './work_item_note_signed_out.vue';
+import WorkItemCommentLocked from './work_item_comment_locked.vue';
+
+export default {
+ constantOptions: {
+ markdownDocsPath: helpPagePath('user/markdown'),
+ avatarUrl: window.gon.current_user_avatar_url,
+ },
+ components: {
+ GlAvatar,
+ GlButton,
+ MarkdownEditor,
+ WorkItemNoteSignedOut,
+ WorkItemCommentLocked,
+ },
+ mixins: [glFeatureFlagMixin(), Tracking.mixin()],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ workItem: {},
+ isEditing: false,
+ isSubmitting: false,
+ isSubmittingWithKeydown: false,
+ commentText: '',
+ };
+ },
+ apollo: {
+ workItem: {
+ query() {
+ return getWorkItemQuery(this.fetchByIid);
+ },
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ skip() {
+ return !this.queryVariables.id && !this.queryVariables.iid;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+ computed: {
+ signedIn() {
+ return Boolean(window.gon.current_user_id);
+ },
+ autosaveKey() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.workItemId}-comment`;
+ },
+ canEdit() {
+ // maybe this should use `NotePermissions.updateNote`, but if
+ // we don't have any notes yet, that permission isn't on WorkItem
+ return Boolean(this.workItem?.userPermissions?.updateWorkItem);
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_comment',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ workItemType() {
+ return this.workItem?.workItemType?.name;
+ },
+ markdownPreviewPath() {
+ return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
+ this.workItemType
+ }`;
+ },
+ isProjectArchived() {
+ return this.workItem?.project?.archived;
+ },
+ },
+ methods: {
+ startEditing() {
+ this.isEditing = true;
+ this.commentText = getDraft(this.autosaveKey) || '';
+ },
+ async cancelEditing() {
+ if (this.commentText) {
+ const msg = s__('WorkItem|Are you sure you want to cancel editing?');
+
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: __('Discard changes'),
+ cancelBtnText: __('Continue editing'),
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ }
+
+ this.isEditing = false;
+ clearDraft(this.autosaveKey);
+ },
+ async updateWorkItem(event = {}) {
+ const { key } = event;
+
+ if (key) {
+ this.isSubmittingWithKeydown = true;
+ }
+
+ this.isSubmitting = true;
+
+ try {
+ this.track('add_work_item_comment');
+
+ const {
+ data: { createNote },
+ } = await this.$apollo.mutate({
+ mutation: createNoteMutation,
+ variables: {
+ input: {
+ noteableId: this.workItem.id,
+ body: this.commentText,
+ },
+ },
+ });
+
+ if (createNote.errors?.length) {
+ throw new Error(createNote.errors[0]);
+ }
+
+ const client = this.$apollo.provider.defaultClient;
+ client.refetchQueries({
+ include: [getWorkItemNotesQuery(this.fetchByIid)],
+ });
+
+ this.isEditing = false;
+ clearDraft(this.autosaveKey);
+ } catch (error) {
+ this.$emit('error', error.message);
+ Sentry.captureException(error);
+ }
+
+ this.isSubmitting = false;
+ },
+ setCommentText(newText) {
+ this.commentText = newText;
+ updateDraft(this.autosaveKey, this.commentText);
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="timeline-entry">
+ <work-item-note-signed-out v-if="!signedIn" />
+ <work-item-comment-locked
+ v-else-if="!canEdit"
+ :work-item-type="workItemType"
+ :is-project-archived="isProjectArchived"
+ />
+ <div v-else class="gl-display-flex gl-align-items-flex-start gl-flex-wrap-nowrap">
+ <gl-avatar :src="$options.constantOptions.avatarUrl" :size="32" class="gl-mr-3" />
+ <form v-if="isEditing" class="common-note-form gfm-form js-main-target-form gl-flex-grow-1">
+ <markdown-editor
+ class="gl-mb-3"
+ :value="commentText"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="$options.constantOptions.markdownDocsPath"
+ :form-field-aria-label="__('Add a comment')"
+ :form-field-placeholder="__('Write a comment or drag your files here…')"
+ form-field-id="work-item-add-comment"
+ form-field-name="work-item-add-comment"
+ enable-autocomplete
+ autofocus
+ use-bottom-toolbar
+ @input="setCommentText"
+ @keydown.meta.enter="updateWorkItem"
+ @keydown.ctrl.enter="updateWorkItem"
+ @keydown.esc="cancelEditing"
+ />
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ @click="updateWorkItem"
+ >{{ __('Comment') }}
+ </gl-button>
+ <gl-button category="tertiary" class="gl-ml-3" @click="cancelEditing"
+ >{{ __('Cancel') }}
+ </gl-button>
+ </form>
+ <gl-button
+ v-else
+ class="gl-flex-grow-1 gl-justify-content-start! gl-text-secondary!"
+ @click="startEditing"
+ >{{ __('Add a comment') }}</gl-button
+ >
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_comment_locked.vue b/app/assets/javascripts/work_items/components/work_item_comment_locked.vue
new file mode 100644
index 00000000000..f837d025b7f
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_comment_locked.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { __, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { TASK_TYPE_NAME } from '~/work_items/constants';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ workItemType: {
+ required: false,
+ type: String,
+ default: TASK_TYPE_NAME,
+ },
+ isProjectArchived: {
+ required: false,
+ type: Boolean,
+ default: false,
+ },
+ },
+ constantOptions: {
+ archivedProjectDocsPath: helpPagePath('user/project/settings/index.md', {
+ anchor: 'archive-a-project',
+ }),
+ lockedIssueDocsPath: helpPagePath('user/discussions/index.md', {
+ anchor: 'prevent-comments-by-locking-the-discussion',
+ }),
+ projectArchivedWarning: __('This project is archived and cannot be commented on.'),
+ },
+ computed: {
+ issuableDisplayName() {
+ return this.workItemType.replace(/_/g, ' ');
+ },
+ lockedIssueWarning() {
+ return sprintf(
+ __('This %{issuableDisplayName} is locked. Only project members can comment.'),
+ { issuableDisplayName: this.issuableDisplayName },
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ <span class="issuable-note-warning gl-display-inline-block">
+ <gl-icon name="lock" class="gl-mr-2" />
+ <template v-if="isProjectArchived">
+ {{ $options.constantOptions.projectArchivedWarning }}
+ <gl-link :href="$options.constantOptions.archivedProjectDocsPath" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
+ </template>
+
+ <template v-else>
+ {{ lockedIssueWarning }}
+ <gl-link :href="$options.constantOptions.lockedIssueDocsPath" class="learn-more">
+ {{ __('Learn more') }}
+ </gl-link>
+ </template>
+ </span>
+ </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 cb45a05de89..ade954b2a7f 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -15,10 +15,14 @@ import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg
import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
+import { isPositiveInteger } from '~/lib/utils/number_utils';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import {
+ sprintfWorkItem,
i18n,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_LABELS,
@@ -53,6 +57,7 @@ import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
import WorkItemNotes from './work_item_notes.vue';
+import WorkItemDetailModal from './work_item_detail_modal.vue';
export default {
i18n,
@@ -83,6 +88,7 @@ export default {
WorkItemMilestone,
WorkItemTree,
WorkItemNotes,
+ WorkItemDetailModal,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath'],
@@ -109,11 +115,16 @@ export default {
},
},
data() {
+ const workItemId = getParameterByName('work_item_id');
+
return {
error: undefined,
updateError: undefined,
workItem: {},
updateInProgress: false,
+ modalWorkItemId: isPositiveInteger(workItemId)
+ ? convertToGraphQLId(TYPE_WORK_ITEM, workItemId)
+ : null,
};
},
apollo: {
@@ -207,6 +218,9 @@ export default {
canDelete() {
return this.workItem?.userPermissions?.deleteWorkItem;
},
+ confidentialTooltip() {
+ return sprintfWorkItem(this.$options.i18n.confidentialTooltip, this.workItemType);
+ },
fullPath() {
return this.workItem?.project.fullPath;
},
@@ -295,6 +309,11 @@ export default {
return widgetHierarchy.children.nodes;
},
},
+ mounted() {
+ if (this.modalWorkItemId) {
+ this.openInModal(undefined, { id: this.modalWorkItemId });
+ }
+ },
methods: {
isWidgetPresent(type) {
return this.workItem?.widgets?.find((widget) => widget.type === type);
@@ -362,9 +381,10 @@ export default {
});
const newData = produce(sourceData, (draftState) => {
- const widgetHierarchy = draftState.workItem.widgets.find(
- (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
- );
+ const widgets = this.fetchByIid
+ ? draftState.workspace.workItems.nodes[0].widgets
+ : draftState.workItem.widgets;
+ const widgetHierarchy = widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
const index = widgetHierarchy.children.nodes.findIndex((child) => child.id === childId);
@@ -419,6 +439,26 @@ export default {
Sentry.captureException(error);
}
},
+ updateUrl(modalWorkItemId) {
+ updateHistory({
+ url: setUrlParams({ work_item_id: getIdFromGraphQLId(modalWorkItemId) }),
+ replace: true,
+ });
+ },
+ openInModal(event, modalWorkItem) {
+ if (event) {
+ event.preventDefault();
+
+ this.updateUrl(modalWorkItem.id);
+ }
+
+ if (this.isModal) {
+ this.$emit('update-modal', event, modalWorkItem.id);
+ return;
+ }
+ this.modalWorkItemId = modalWorkItem.id;
+ this.$refs.modal.show();
+ },
},
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
};
@@ -456,6 +496,7 @@ export default {
category="tertiary"
:href="parentUrl"
:title="parentWorkItem.title"
+ @click="openInModal($event, parentWorkItem)"
>{{ parentWorkItem.title }}</gl-button
>
<gl-icon name="chevron-right" :size="16" class="gl-flex-shrink-0" />
@@ -482,7 +523,7 @@ export default {
<gl-badge
v-if="workItem.confidential"
v-gl-tooltip.bottom
- :title="$options.i18n.confidentialTooltip"
+ :title="confidentialTooltip"
variant="warning"
icon="eye-slash"
class="gl-mr-3 gl-cursor-help"
@@ -605,6 +646,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"
/>
<work-item-description
@@ -619,20 +663,24 @@ export default {
<work-item-tree
v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
:work-item-type="workItemType"
+ :parent-work-item-type="workItem.workItemType.name"
:work-item-id="workItem.id"
:children="children"
:can-update="canUpdate"
:project-path="fullPath"
+ :confidential="workItem.confidential"
@addWorkItemChild="addChild"
@removeChild="removeChild"
+ @show-modal="openInModal"
/>
- <template v-if="workItemsMvc2Enabled">
+ <template v-if="workItemsMvcEnabled">
<work-item-notes
v-if="workItemNotes"
:work-item-id="workItem.id"
:query-variables="queryVariables"
:full-path="fullPath"
:fetch-by-iid="fetchByIid"
+ :work-item-type="workItemType"
class="gl-pt-5"
@error="updateError = $event"
/>
@@ -644,5 +692,12 @@ export default {
:svg-path="noAccessSvgPath"
/>
</template>
+ <work-item-detail-modal
+ v-if="!isModal"
+ ref="modal"
+ :work-item-id="modalWorkItemId"
+ :show="true"
+ @close="updateUrl"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index e8726814aaf..faea80a9de8 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -3,7 +3,6 @@ import { GlAlert, GlModal } from '@gitlab/ui';
import { s__ } from '~/locale';
import deleteWorkItemFromTaskMutation from '../graphql/delete_task_from_work_item.mutation.graphql';
import deleteWorkItemMutation from '../graphql/delete_work_item.mutation.graphql';
-import WorkItemDetail from './work_item_detail.vue';
export default {
i18n: {
@@ -12,7 +11,7 @@ export default {
components: {
GlAlert,
GlModal,
- WorkItemDetail,
+ WorkItemDetail: () => import('./work_item_detail.vue'),
},
props: {
workItemId: {
@@ -46,12 +45,18 @@ export default {
default: null,
},
},
- emits: ['workItemDeleted', 'close'],
+ emits: ['workItemDeleted', 'close', 'update-modal'],
data() {
return {
error: undefined,
+ updatedWorkItemId: null,
};
},
+ computed: {
+ displayedWorkItemId() {
+ return this.updatedWorkItemId || this.workItemId;
+ },
+ },
methods: {
deleteWorkItem() {
if (this.lockVersion != null && this.lineNumberStart && this.lineNumberEnd) {
@@ -116,6 +121,7 @@ export default {
});
},
closeModal() {
+ this.updatedWorkItemId = null;
this.error = '';
this.$emit('close');
},
@@ -128,6 +134,10 @@ export default {
show() {
this.$refs.modal.show();
},
+ updateModal($event, workItemId) {
+ this.updatedWorkItemId = workItemId;
+ this.$emit('update-modal', $event, workItemId);
+ },
},
};
</script>
@@ -149,11 +159,12 @@ export default {
<work-item-detail
is-modal
:work-item-parent-id="issueGid"
- :work-item-id="workItemId"
+ :work-item-id="displayedWorkItemId"
:work-item-iid="workItemIid"
- class="gl-p-5 gl-mt-n3"
+ class="gl-p-5 gl-mt-n3 gl-reset-bg gl-isolate"
@close="hide"
@deleteWorkItem="deleteWorkItem"
+ @update-modal="updateModal"
/>
</gl-modal>
</template>
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 edad0e9b616..a7405b6d86c 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
@@ -18,6 +18,8 @@ export default function initWorkItemLinks() {
iid,
wiHasIterationsFeature,
wiHasIssuableHealthStatusFeature,
+ registerPath,
+ signInPath,
} = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
@@ -35,6 +37,8 @@ export default function initWorkItemLinks() {
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
+ registerPath,
+ signInPath,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 763f2f338a3..3a3a846bce5 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -5,11 +5,14 @@ import { __, s__ } from '~/locale';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
import {
STATE_OPEN,
TASK_TYPE_NAME,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WIDGET_TYPE_PROGRESS,
+ WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_HIERARCHY,
WIDGET_TYPE_ASSIGNEES,
@@ -17,7 +20,6 @@ import {
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
-import WorkItemLinkChildMetadata from './work_item_link_child_metadata.vue';
import WorkItemLinksMenu from './work_item_links_menu.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
@@ -73,8 +75,15 @@ export default {
canHaveChildren() {
return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE;
},
- allowsScopedLabels() {
- return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.allowsScopedLabels;
+ metadataWidgets() {
+ return this.childItem.widgets?.reduce((metadataWidgets, widget) => {
+ // Skip Hierarchy widget as it is not part of metadata.
+ if (widget.type && widget.type !== WIDGET_TYPE_HIERARCHY) {
+ // eslint-disable-next-line no-param-reassign
+ metadataWidgets[widget.type] = widget;
+ }
+ return metadataWidgets;
+ }, {});
},
isItemOpen() {
return this.childItem.state === STATE_OPEN;
@@ -113,16 +122,16 @@ export default {
return this.isExpanded ? __('Collapse') : __('Expand');
},
hasMetadata() {
- return this.milestone || this.assignees.length > 0 || this.labels.length > 0;
- },
- milestone() {
- return this.getWidgetByType(this.childItem, WIDGET_TYPE_MILESTONE)?.milestone;
- },
- assignees() {
- return this.getWidgetByType(this.childItem, WIDGET_TYPE_ASSIGNEES)?.assignees?.nodes || [];
- },
- labels() {
- return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.labels?.nodes || [];
+ if (this.metadataWidgets) {
+ return (
+ Number.isInteger(this.metadataWidgets[WIDGET_TYPE_PROGRESS]?.progress) ||
+ Boolean(this.metadataWidgets[WIDGET_TYPE_HEALTH_STATUS]?.healthStatus) ||
+ Boolean(this.metadataWidgets[WIDGET_TYPE_MILESTONE]?.milestone) ||
+ this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes.length > 0 ||
+ this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes.length > 0
+ );
+ }
+ return false;
},
},
methods: {
@@ -230,10 +239,7 @@ export default {
</div>
<work-item-link-child-metadata
v-if="hasMetadata"
- :allows-scoped-labels="allowsScopedLabels"
- :milestone="milestone"
- :assignees="assignees"
- :labels="labels"
+ :metadata-widgets="metadataWidgets"
class="gl-mt-3"
/>
</div>
@@ -258,6 +264,7 @@ export default {
:work-item-type="workItemType"
:children="children"
@removeChild="fetchChildren"
+ @click="$emit('click', $event)"
/>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
index 7be7e1f3496..6974804523a 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
@@ -6,6 +6,8 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
+import { WIDGET_TYPE_MILESTONE, WIDGET_TYPE_ASSIGNEES, WIDGET_TYPE_LABELS } from '../../constants';
+
export default {
components: {
GlLabel,
@@ -18,28 +20,25 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
- allowsScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
- milestone: {
+ metadataWidgets: {
type: Object,
required: false,
- default: null,
- },
- assignees: {
- type: Array,
- required: false,
- default: () => [],
- },
- labels: {
- type: Array,
- required: false,
- default: () => [],
+ default: () => ({}),
},
},
computed: {
+ milestone() {
+ return this.metadataWidgets[WIDGET_TYPE_MILESTONE]?.milestone;
+ },
+ assignees() {
+ return this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes || [];
+ },
+ labels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
+ },
+ allowsScopedLabels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
+ },
assigneesCollapsedTooltip() {
if (this.assignees.length > 2) {
return sprintf(s__('WorkItem|%{count} more assignees'), {
@@ -56,12 +55,6 @@ export default {
}
return '';
},
- labelsContainerClass() {
- if (this.milestone || this.assignees.length) {
- return 'gl-sm-ml-5';
- }
- return '';
- },
},
methods: {
showScopedLabel(label) {
@@ -73,6 +66,7 @@ export default {
<template>
<div class="gl-display-flex gl-flex-wrap gl-align-items-center">
+ <slot></slot>
<item-milestone
v-if="milestone"
:milestone="milestone"
@@ -87,6 +81,7 @@ export default {
badge-tooltip-prop="name"
:badge-sr-only-text="assigneesCollapsedTooltip"
:class="assigneesContainerClass"
+ class="gl-mr-5"
>
<template #avatar="{ avatar }">
<gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
@@ -94,7 +89,7 @@ export default {
</gl-avatar-link>
</template>
</gl-avatars-inline>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap" :class="labelsContainerClass">
+ <div v-if="labels.length" class="gl-display-flex gl-flex-wrap">
<gl-label
v-for="label in labels"
:key="label.id"
@@ -102,7 +97,7 @@ export default {
:background-color="label.color"
:description="label.description"
:scoped="showScopedLabel(label)"
- class="gl-mt-2 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm"
+ class="gl-mt-3 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm"
tooltip-placement="top"
/>
</div>
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 faadb5fa6fa..b078711ec5d 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
@@ -399,6 +399,7 @@ export default {
:parent-iteration="issuableIteration"
:parent-milestone="issuableMilestone"
:form-type="formType"
+ :parent-work-item-type="workItem.workItemType.name"
@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 5cf0c4154bb..d79aaab38f2 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,9 +1,18 @@
<script>
-import { GlAlert, GlFormGroup, GlForm, GlTokenSelector, GlButton, GlFormInput } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlFormGroup,
+ GlForm,
+ GlTokenSelector,
+ GlButton,
+ GlFormInput,
+ GlFormCheckbox,
+ GlTooltip,
+} from '@gitlab/ui';
import { debounce } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { __, s__ } from '~/locale';
+import { __, s__, sprintf } 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';
@@ -17,6 +26,8 @@ import {
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_ADD_BUTTON_LABEL,
I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL,
+ I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
+ I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP,
sprintfWorkItem,
} from '../../constants';
@@ -28,6 +39,8 @@ export default {
GlButton,
GlFormGroup,
GlFormInput,
+ GlFormCheckbox,
+ GlTooltip,
},
mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'hasIterationsFeature'],
@@ -61,6 +74,11 @@ export default {
type: String,
required: true,
},
+ parentWorkItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
childrenType: {
type: String,
required: false,
@@ -108,6 +126,7 @@ export default {
error: null,
childToCreateTitle: null,
workItemsToAdd: [],
+ confidential: this.parentConfidential,
};
},
computed: {
@@ -119,7 +138,7 @@ export default {
hierarchyWidget: {
parentId: this.issuableGid,
},
- confidential: this.parentConfidential,
+ confidential: this.parentConfidential || this.confidential,
};
if (this.parentMilestoneId) {
@@ -154,6 +173,9 @@ export default {
childrenTypeName() {
return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
},
+ childrenTypeValue() {
+ return WORK_ITEMS_TYPE_MAP[this.childrenType]?.value;
+ },
addOrCreateButtonLabel() {
if (this.isCreateForm) {
return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName);
@@ -162,11 +184,24 @@ export default {
}
return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
},
+ confidentialityCheckboxLabel() {
+ return sprintfWorkItem(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, this.childrenTypeName);
+ },
+ confidentialityCheckboxTooltip() {
+ return sprintfWorkItem(
+ I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP,
+ this.childrenTypeName,
+ this.parentWorkItemType,
+ );
+ },
+ showConfidentialityTooltip() {
+ return this.isCreateForm && this.parentConfidential;
+ },
addOrCreateMethod() {
return this.isCreateForm ? this.createChild : this.addChild;
},
childWorkItemType() {
- return this.workItemTypes.find((type) => type.name === this.childrenTypeName)?.id;
+ return this.workItemTypes.find((type) => type.name === this.childrenTypeValue)?.id;
},
parentIterationId() {
return this.parentIteration?.id;
@@ -178,7 +213,10 @@ export default {
return this.parentMilestone?.id;
},
isSubmitButtonDisabled() {
- return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
+ if (this.isCreateForm) {
+ return this.search.length === 0;
+ }
+ return this.workItemsToAdd.length === 0 || !this.areWorkItemsToAddValid;
},
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
@@ -186,12 +224,43 @@ export default {
addInputPlaceholder() {
return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
},
+ tokenSelectorContainerClass() {
+ return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
+ },
+ invalidWorkItemsToAdd() {
+ return this.parentConfidential
+ ? this.workItemsToAdd.filter((workItem) => !workItem.confidential)
+ : [];
+ },
+ areWorkItemsToAddValid() {
+ return this.invalidWorkItemsToAdd.length === 0;
+ },
+ showWorkItemsToAddInvalidMessage() {
+ return !this.isCreateForm && !this.areWorkItemsToAddValid;
+ },
+ workItemsToAddInvalidMessage() {
+ return sprintf(
+ s__(
+ 'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
+ ),
+ {
+ invalidWorkItemsList: this.invalidWorkItemsToAdd.map(({ title }) => title).join(', '),
+ childWorkItemType: this.childrenTypeName,
+ parentWorkItemType: this.parentWorkItemType,
+ },
+ );
+ },
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
getIdFromGraphQLId,
+ getConfidentialityTooltipTarget() {
+ // We want tooltip to be anchored to `input` within checkbox component
+ // but `$el.querySelector('input')` doesn't work. 🤷‍♂️
+ return this.$refs.confidentialityCheckbox?.$el;
+ },
unsetError() {
this.error = null;
},
@@ -299,30 +368,54 @@ export default {
autofocus
/>
</gl-form-group>
- <gl-token-selector
- v-else
- v-model="workItemsToAdd"
- :dropdown-items="availableWorkItems"
- :loading="isLoading"
- :placeholder="addInputPlaceholder"
- 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"
+ <gl-form-checkbox
+ v-if="isCreateForm"
+ ref="confidentialityCheckbox"
+ v-model="confidential"
+ name="isConfidential"
+ class="gl-md-mt-5 gl-mb-5 gl-md-mb-3!"
+ :disabled="parentConfidential"
+ >{{ confidentialityCheckboxLabel }}</gl-form-checkbox
+ >
+ <gl-tooltip
+ v-if="showConfidentialityTooltip"
+ :target="getConfidentialityTooltipTarget"
+ triggers="hover"
+ >{{ confidentialityCheckboxTooltip }}</gl-tooltip
>
- <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>
+ <div class="gl-mb-4">
+ <gl-token-selector
+ v-if="!isCreateForm"
+ v-model="workItemsToAdd"
+ :dropdown-items="availableWorkItems"
+ :loading="isLoading"
+ :placeholder="addInputPlaceholder"
+ menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
+ :container-class="tokenSelectorContainerClass"
+ data-testid="work-item-token-select-input"
+ @text-input="debouncedSearchKeyUpdate"
+ @focus="handleFocus"
+ @mouseover.native="handleMouseOver"
+ @mouseout.native="handleMouseOut"
+ >
+ <template #token-content="{ token }">
+ {{ token.title }}
+ </template>
+ <template #dropdown-item-content="{ dropdownItem }">
+ <div class="gl-display-flex">
+ <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(dropdownItem.id) }}</div>
+ <div class="gl-text-truncate">{{ dropdownItem.title }}</div>
+ </div>
+ </template>
+ </gl-token-selector>
+ <div
+ v-if="showWorkItemsToAddInvalidMessage"
+ class="gl-text-red-500"
+ data-testid="work-items-invalid"
+ >
+ {{ workItemsToAddInvalidMessage }}
+ </div>
+ </div>
<gl-button
category="primary"
variant="confirm"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index f06de2ca048..81e2bb76900 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -40,10 +40,20 @@ export default {
type: String,
required: true,
},
+ parentWorkItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
workItemId: {
type: String,
required: true,
},
+ confidential: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
children: {
type: Array,
required: false,
@@ -221,8 +231,10 @@ export default {
data-testid="add-tree-form"
:issuable-gid="workItemId"
:form-type="formType"
+ :parent-work-item-type="parentWorkItemType"
:children-type="childType"
:children-ids="childrenIds"
+ :parent-confidential="confidential"
@addWorkItemChild="$emit('addWorkItemChild', $event)"
@cancel="hideAddForm"
/>
@@ -233,11 +245,13 @@ export default {
:can-update="canUpdate"
:issuable-gid="workItemId"
:child-item="child"
+ :confidential="child.confidential"
:work-item-type="workItemType"
:has-indirect-children="hasIndirectChildren"
@mouseover="prefetchWorkItem(child)"
@mouseout="clearPrefetching"
@removeChild="$emit('removeChild', $event)"
+ @click="$emit('show-modal', $event, $event.childItem || child)"
/>
</div>
</div>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
index 911cac4de88..71de6867680 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -63,6 +63,7 @@ export default {
:child-item="child"
:work-item-type="workItemType"
@removeChild="updateWorkItem"
+ @click="$emit('click', Object.assign($event, { childItem: child }))"
/>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_note_signed_out.vue b/app/assets/javascripts/work_items/components/work_item_note_signed_out.vue
new file mode 100644
index 00000000000..3ef4a16bc57
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_note_signed_out.vue
@@ -0,0 +1,31 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { __, sprintf } from '~/locale';
+
+export default {
+ directives: {
+ SafeHtml,
+ },
+ inject: ['registerPath', 'signInPath'],
+ computed: {
+ signedOutText() {
+ return sprintf(
+ __(
+ 'Please %{startTagRegister}register%{endRegisterTag} or %{startTagSignIn}sign in%{endSignInTag} to reply',
+ ),
+ {
+ startTagRegister: `<a href="${this.registerPath}">`,
+ startTagSignIn: `<a href="${this.signInPath}">`,
+ endRegisterTag: '</a>',
+ endSignInTag: '</a>',
+ },
+ false,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-safe-html="signedOutText" class="disabled-comment gl-text-center"></div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 91e90589a93..a59767d8b70 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -2,8 +2,12 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { s__ } from '~/locale';
import SystemNote from '~/work_items/components/notes/system_note.vue';
+import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
+import { ASC, DESC } from '~/notes/constants';
import { getWorkItemNotesQuery } from '~/work_items/utils';
+import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemCommentForm from './work_item_comment_form.vue';
export default {
i18n: {
@@ -15,8 +19,11 @@ export default {
height: 40,
},
components: {
- SystemNote,
GlSkeletonLoader,
+ ActivityFilter,
+ SystemNote,
+ WorkItemCommentForm,
+ WorkItemNote,
},
props: {
workItemId: {
@@ -31,22 +38,50 @@ export default {
type: String,
required: true,
},
+ workItemType: {
+ type: String,
+ required: true,
+ },
fetchByIid: {
type: Boolean,
required: false,
default: false,
},
},
+ data() {
+ return {
+ notesArray: [],
+ isLoadingMore: false,
+ perPage: DEFAULT_PAGE_SIZE_NOTES,
+ sortOrder: ASC,
+ changeNotesSortOrderAfterLoading: false,
+ };
+ },
computed: {
- areNotesLoading() {
- return this.$apollo.queries.workItemNotes.loading;
- },
- notes() {
- return this.workItemNotes?.nodes;
+ initialLoading() {
+ return this.$apollo.queries.workItemNotes.loading && !this.isLoadingMore;
},
pageInfo() {
return this.workItemNotes?.pageInfo;
},
+ avatarUrl() {
+ return window.gon.current_user_avatar_url;
+ },
+ hasNextPage() {
+ return this.pageInfo?.hasNextPage;
+ },
+ showInitialLoader() {
+ return this.initialLoading || this.changeNotesSortOrderAfterLoading;
+ },
+ showTimeline() {
+ return !this.changeNotesSortOrderAfterLoading;
+ },
+ showLoadingMoreSkeleton() {
+ return this.isLoadingMore && !this.changeNotesSortOrderAfterLoading;
+ },
+ disableActivityFilter() {
+ return this.initialLoading || this.isLoadingMore;
+ },
},
apollo: {
workItemNotes: {
@@ -59,6 +94,7 @@ export default {
variables() {
return {
...this.queryVariables,
+ after: this.after,
pageSize: DEFAULT_PAGE_SIZE_NOTES,
};
},
@@ -66,7 +102,11 @@ export default {
const workItemWidgets = this.fetchByIid
? data.workspace?.workItems?.nodes[0]?.widgets
: data.workItem?.widgets;
- return workItemWidgets.find((widget) => widget.type === 'NOTES').discussions || [];
+ const discussionNodes =
+ workItemWidgets.find((widget) => widget.type === 'NOTES')?.discussions || [];
+ this.notesArray = discussionNodes?.nodes || [];
+ this.updateSortingOrderIfApplicable();
+ return discussionNodes;
},
skip() {
return !this.queryVariables.id && !this.queryVariables.iid;
@@ -74,6 +114,58 @@ export default {
error() {
this.$emit('error', i18n.fetchError);
},
+ result() {
+ if (this.hasNextPage) {
+ this.fetchMoreNotes();
+ }
+ },
+ },
+ },
+ methods: {
+ isSystemNote(note) {
+ return note.notes.nodes[0].system;
+ },
+ updateSortingOrderIfApplicable() {
+ // when the sort order is DESC in local storage and there is only a single page, call
+ // changeSortOrder manually
+ if (
+ this.changeNotesSortOrderAfterLoading &&
+ this.perPage === DEFAULT_PAGE_SIZE_NOTES &&
+ !this.hasNextPage
+ ) {
+ this.changeNotesSortOrder(DESC);
+ }
+ },
+ updateInitialSortedOrder(direction) {
+ this.sortOrder = direction;
+ // when the direction is reverse , we need to load all since the sorting is on the frontend
+ if (direction === DESC) {
+ this.changeNotesSortOrderAfterLoading = true;
+ }
+ },
+ changeNotesSortOrder(direction) {
+ this.sortOrder = direction;
+ this.notesArray = [...this.notesArray].reverse();
+ this.changeNotesSortOrderAfterLoading = false;
+ },
+ async fetchMoreNotes() {
+ this.isLoadingMore = true;
+ // copied from discussions batch logic - every fetchMore call has a higher
+ // amount of page size than the previous one with the limit being 100
+ this.perPage = Math.min(Math.round(this.perPage * 1.5), 100);
+ await this.$apollo.queries.workItemNotes
+ .fetchMore({
+ variables: {
+ ...this.queryVariables,
+ pageSize: this.perPage,
+ after: this.pageInfo?.endCursor,
+ },
+ })
+ .catch((error) => this.$emit('error', error.message));
+ this.isLoadingMore = false;
+ if (this.changeNotesSortOrderAfterLoading && !this.hasNextPage) {
+ this.changeNotesSortOrder(this.sortOrder);
+ }
},
},
};
@@ -81,8 +173,18 @@ export default {
<template>
<div class="gl-border-t gl-mt-5">
- <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
- <div v-if="areNotesLoading" class="gl-mt-5">
+ <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
+ <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
+ <activity-filter
+ class="gl-min-h-5 gl-pb-3"
+ :loading="disableActivityFilter"
+ :sort-order="sortOrder"
+ :work-item-type="workItemType"
+ @changeSortOrder="changeNotesSortOrder"
+ @updateSavedSortOrder="updateInitialSortedOrder"
+ />
+ </div>
+ <div v-if="showInitialLoader" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
@@ -94,16 +196,40 @@ export default {
<rect width="500" x="45" y="15" height="10" rx="4" />
</gl-skeleton-loader>
</div>
- <div v-else class="issuable-discussion gl-mb-5 work-item-notes">
- <template v-if="notes && notes.length">
- <ul class="notes main-notes-list timeline">
- <system-note
- v-for="note in notes"
- :key="note.notes.nodes[0].id"
- :note="note.notes.nodes[0]"
+ <div v-else class="issuable-discussion gl-mb-5 gl-clearfix!">
+ <template v-if="showTimeline">
+ <ul class="notes main-notes-list timeline gl-clearfix!">
+ <template v-for="note in notesArray">
+ <system-note
+ v-if="isSystemNote(note)"
+ :key="note.notes.nodes[0].id"
+ :note="note.notes.nodes[0]"
+ />
+ <work-item-note v-else :key="note.notes.nodes[0].id" :note="note.notes.nodes[0]" />
+ </template>
+
+ <work-item-comment-form
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :work-item-id="workItemId"
+ :fetch-by-iid="fetchByIid"
+ @error="$emit('error', $event)"
/>
</ul>
</template>
+
+ <template v-if="showLoadingMoreSkeleton">
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <circle cx="20" cy="20" r="16" />
+ <rect width="500" x="45" y="15" height="10" rx="4" />
+ </gl-skeleton-loader>
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
index 32678e29fa4..96a6493357c 100644
--- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue
+++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue
@@ -33,11 +33,6 @@ export default {
},
computed: {
iconName() {
- // TODO: Remove this once https://gitlab.com/gitlab-org/gitlab-svgs/-/merge_requests/865
- // is merged and updated in GitLab repo.
- if (this.workItemIconName === 'issue-type-keyresult') {
- return 'issue-type-key-result';
- }
return (
this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue'
);
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 3cd17f4d360..81f9bf04bc8 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -31,7 +31,12 @@ export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const WORK_ITEM_TYPE_ENUM_OBJECTIVE = 'OBJECTIVE';
export const WORK_ITEM_TYPE_ENUM_KEY_RESULT = 'KEY_RESULT';
+export const WORK_ITEM_TYPE_VALUE_INCIDENT = 'Incident';
export const WORK_ITEM_TYPE_VALUE_ISSUE = 'Issue';
+export const WORK_ITEM_TYPE_VALUE_TASK = 'Task';
+export const WORK_ITEM_TYPE_VALUE_TEST_CASE = 'Test case';
+export const WORK_ITEM_TYPE_VALUE_REQUIREMENTS = 'Requirements';
+export const WORK_ITEM_TYPE_VALUE_KEY_RESULT = 'Key Result';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
export const i18n = {
@@ -41,7 +46,7 @@ export const i18n = {
),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
confidentialTooltip: s__(
- 'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this task.',
+ 'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this %{workItemType}.',
),
};
@@ -73,12 +78,19 @@ export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{work
export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
'WorkItem|Search existing %{workItemType}s',
);
+export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__(
+ 'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access',
+);
+export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP = s__(
+ 'WorkItem|A non-confidential %{workItemType} cannot be assigned to a confidential parent %{parentWorkItemType}.',
+);
-export const sprintfWorkItem = (msg, workItemTypeArg) => {
+export const sprintfWorkItem = (msg, workItemTypeArg, parentWorkItemType = '') => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
sprintf(msg, {
workItemType: workItemType.toLocaleLowerCase(),
+ parentWorkItemType: parentWorkItemType.toLocaleLowerCase(),
}),
);
};
@@ -96,30 +108,37 @@ export const WORK_ITEMS_TYPE_MAP = {
[WORK_ITEM_TYPE_ENUM_INCIDENT]: {
icon: `issue-type-incident`,
name: s__('WorkItem|Incident'),
+ value: WORK_ITEM_TYPE_VALUE_INCIDENT,
},
[WORK_ITEM_TYPE_ENUM_ISSUE]: {
icon: `issue-type-issue`,
name: s__('WorkItem|Issue'),
+ value: WORK_ITEM_TYPE_VALUE_ISSUE,
},
[WORK_ITEM_TYPE_ENUM_TASK]: {
icon: `issue-type-task`,
name: s__('WorkItem|Task'),
+ value: WORK_ITEM_TYPE_VALUE_TASK,
},
[WORK_ITEM_TYPE_ENUM_TEST_CASE]: {
icon: `issue-type-test-case`,
name: s__('WorkItem|Test case'),
+ value: WORK_ITEM_TYPE_VALUE_TEST_CASE,
},
[WORK_ITEM_TYPE_ENUM_REQUIREMENTS]: {
icon: `issue-type-requirements`,
name: s__('WorkItem|Requirements'),
+ value: WORK_ITEM_TYPE_VALUE_REQUIREMENTS,
},
[WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
icon: `issue-type-objective`,
name: s__('WorkItem|Objective'),
+ value: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
},
[WORK_ITEM_TYPE_ENUM_KEY_RESULT]: {
- icon: `issue-type-issue`,
+ icon: `issue-type-keyresult`,
name: s__('WorkItem|Key Result'),
+ value: WORK_ITEM_TYPE_VALUE_KEY_RESULT,
},
};
@@ -141,7 +160,7 @@ export const WORK_ITEM_NAME_TO_ICON_MAP = {
Task: 'issue-type-task',
Objective: 'issue-type-objective',
// eslint-disable-next-line @gitlab/require-i18n-strings
- 'Key Result': 'issue-type-key-result',
+ 'Key Result': 'issue-type-keyresult',
};
export const FORM_TYPES = {
@@ -154,4 +173,6 @@ export const FORM_TYPES = {
};
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
-export const DEFAULT_PAGE_SIZE_NOTES = 100;
+export const DEFAULT_PAGE_SIZE_NOTES = 30;
+
+export const WORK_ITEM_NOTES_SORT_ORDER_KEY = 'sort_direction_work_item';
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql
new file mode 100644
index 00000000000..6a7afd7bd5b
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/create_work_item_note.mutation.graphql
@@ -0,0 +1,5 @@
+mutation createWorkItemNote($input: CreateNoteInput!) {
+ createNote(input: $input) {
+ errors
+ }
+}
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 3a23db3886a..fce10f6f2a6 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
@@ -11,6 +11,7 @@ query projectWorkItems(
id
title
state
+ confidential
}
}
}
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 6a81cc230b1..3ee263c149d 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -12,6 +12,7 @@ fragment WorkItem on WorkItem {
project {
id
fullPath
+ archived
}
workItemType {
id
diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
index 7fcf622cdb2..7d7bb9c7fc5 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
@@ -3,6 +3,7 @@ query workItemLinksQuery($id: WorkItemID!) {
id
workItemType {
id
+ name
}
title
userPermissions {
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
index baefcdaea93..b7813ca4dc6 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -19,7 +19,6 @@ fragment WorkItemMetadataWidgets on WorkItemWidget {
}
... on WorkItemWidgetLabels {
type
- allowsScopedLabels
labels {
nodes {
...Label
diff --git a/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_note.fragment.graphql
index 62ced6bdfea..5215ea10918 100644
--- a/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_note.fragment.graphql
@@ -1,12 +1,16 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
-fragment Discussion on Note {
+fragment WorkItemNote on Note {
id
- body
bodyHtml
+ system
+ internal
systemNoteIconName
createdAt
author {
...User
}
+ userPermissions {
+ adminNote
+ }
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
index 9439f22f955..9ea9cecc81a 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "~/work_items/graphql/discussion.fragment.graphql"
+#import "~/work_items/graphql/work_item_note.fragment.graphql"
query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
workItem(id: $id) {
@@ -8,7 +8,7 @@ query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
widgets {
... on WorkItemWidgetNotes {
type
- discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ discussions(first: $pageSize, after: $after, filter: ALL_NOTES) {
pageInfo {
...PageInfo
}
@@ -16,7 +16,7 @@ query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
id
notes {
nodes {
- ...Discussion
+ ...WorkItemNote
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
index 3e0960f3f54..f401aa5595e 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
@@ -1,5 +1,5 @@
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-#import "~/work_items/graphql/discussion.fragment.graphql"
+#import "~/work_items/graphql/work_item_note.fragment.graphql"
query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
workspace: project(fullPath: $fullPath) {
@@ -11,7 +11,7 @@ query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize
widgets {
... on WorkItemWidgetNotes {
type
- discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ discussions(first: $pageSize, after: $after, filter: ALL_NOTES) {
pageInfo {
...PageInfo
}
@@ -19,7 +19,7 @@ query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize
id
notes {
nodes {
- ...Discussion
+ ...WorkItemNote
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
index 006ca29e01c..b4fb83b24c2 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
@@ -1,6 +1,6 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
-#import "./work_item_metadata_widgets.fragment.graphql"
+#import "ee_else_ce/work_items/graphql/work_item_metadata_widgets.fragment.graphql"
query workItemTreeQuery($id: WorkItemID!) {
workItem(id: $id) {
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 cf3374e1737..d2a2d7927d3 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,7 +1,7 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/work_items/graphql/milestone.fragment.graphql"
-#import "./work_item_metadata_widgets.fragment.graphql"
+#import "ee_else_ce/work_items/graphql/work_item_metadata_widgets.fragment.graphql"
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index a056fde6928..98b59449af7 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -10,6 +10,8 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature,
issuesListPath,
+ registerPath,
+ signInPath,
hasIterationsFeature,
hasOkrsFeature,
hasIssuableHealthStatusFeature,
@@ -26,6 +28,8 @@ export const initWorkItemsRoot = () => {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
+ registerPath,
+ signInPath,
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
},
diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss
index a6ecca88bd4..a023b41083d 100644
--- a/app/assets/stylesheets/fonts.scss
+++ b/app/assets/stylesheets/fonts.scss
@@ -26,7 +26,36 @@ Usage:
src: font-url('jetbrains-mono/JetBrainsMono.woff2') format('woff2');
}
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-display: optional;
+ font-weight: bold;
+ src: font-url('jetbrains-mono/JetBrainsMono-Bold.woff2') format('woff2');
+}
+
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-display: optional;
+ font-weight: normal;
+ font-style: italic;
+ src: font-url('jetbrains-mono/JetBrainsMono-Italic.woff2') format('woff2');
+}
+
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-display: optional;
+ font-weight: bold;
+ font-style: italic;
+ src: font-url('jetbrains-mono/JetBrainsMono-BoldItalic.woff2') format('woff2');
+}
+
:root {
--default-mono-font: 'JetBrains Mono', 'Menlo';
--default-regular-font: 'GitLab Sans', -apple-system;
}
+
+// This isn't the best solution, but we needed a quick fix
+// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107592/
+* {
+ font-variant-ligatures: none;
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 07db6b3c147..e60353578b0 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -38,6 +38,7 @@
@import 'framework/sidebar';
@import 'framework/contextual_sidebar_header';
@import 'framework/contextual_sidebar';
+@import 'framework/super_sidebar';
@import 'framework/tables';
@import 'framework/notes';
@import 'framework/tabs';
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 14e756a5c21..0bc920b1f73 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -471,6 +471,10 @@ img.emoji {
.gl-font-size-28 { font-size: $gl-font-size-28; }
.gl-font-size-42 { font-size: $gl-font-size-42; }
+.gl-icon-button:hover {
+ background-color: $gray-100;
+}
+
.border-section {
@include gl-py-6;
@include gl-m-0;
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index 34c7ffa58fe..1e05441c731 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -138,13 +138,12 @@
}
@mixin top-level-item {
+ @include gl-h-7;
@include gl-px-4;
- @include gl-py-3;
@include gl-display-flex;
@include gl-align-items-center;
@include gl-rounded-base;
@include gl-w-auto;
- @include gl-line-height-normal;
transition: none;
margin: $sidebar-top-item-tb-margin $sidebar-top-item-lr-margin;
@@ -339,6 +338,7 @@
a {
@include top-level-item;
@include context-header;
+ @include gl-h-auto;
}
}
}
@@ -348,6 +348,7 @@
.context-header a {
@include context-header;
+ @include gl-h-auto;
}
> li {
@@ -457,9 +458,9 @@
// PANELS-SPECIFIC
//
+.icon-avatar,
.settings-avatar {
svg {
margin: auto;
}
}
-
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 0acda85f527..65d7eafb8b8 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -851,10 +851,6 @@
@include gl-focus($inset: true);
}
}
-
- .frequent-items-list-item-container a {
- display: flex;
- }
}
.section-header {
@@ -873,9 +869,10 @@
.frequent-items-item-title,
.frequent-items-item-namespace {
- max-width: 250px;
+ max-width: 220px;
text-overflow: ellipsis;
white-space: nowrap;
+ overflow: hidden;
}
.frequent-items-item-title {
@@ -895,6 +892,11 @@
.frequent-items-item-metadata-container {
float: none;
}
+
+ .frequent-items-item-title,
+ .frequent-items-item-namespace {
+ max-width: 250px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index ea741af918c..98083fbc72a 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -37,3 +37,14 @@
}
}
}
+
+.approvers-select {
+ .dropdown-menu {
+ @include gl-w-full;
+ @include gl-max-w-none;
+ }
+
+ .gl-dropdown-item-check-icon {
+ @include gl-display-none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
new file mode 100644
index 00000000000..59a9df9ede0
--- /dev/null
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -0,0 +1,22 @@
+.super-sidebar {
+ top: 0;
+ width: $contextual-sidebar-width;
+
+ .user-bar {
+ background-color: $t-gray-a-04;
+
+ .tanuki-logo {
+ @include gl-vertical-align-middle;
+ }
+ }
+
+ .context-switcher-toggle {
+ &[aria-expanded='true'] {
+ background-color: $t-gray-a-08;
+ }
+ }
+}
+
+.with-performance-bar .super-sidebar {
+ top: $performance-bar-height;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ec8ffaf8c53..539e92eeca4 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -392,7 +392,7 @@ $gl-font-size-large: 16px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
$gl-text-color: $gray-900;
-$gl-text-color-secondary: $gray-500;
+$gl-text-color-secondary: $gray-500 !default;
$gl-text-color-tertiary: $gray-400;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: $white;
diff --git a/app/assets/stylesheets/lazy_bundles/select2_overrides.scss b/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
index 2c5ea8347ae..e3cec187fab 100644
--- a/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
+++ b/app/assets/stylesheets/lazy_bundles/select2_overrides.scss
@@ -339,19 +339,3 @@
display: inline;
}
}
-
-.gl-select2-html5-required-fix {
- .select2-container {
- + .select2 {
- @include gl-opacity-0;
- @include gl-border-0;
- @include gl-bg-none;
- @include gl-bg-transparent;
- display: block !important;
- width: 1px;
- height: 1px;
- z-index: -1;
- margin: -3px auto 0;
- }
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/ci_status.scss b/app/assets/stylesheets/page_bundles/ci_status.scss
index 6b976106cc9..7adbf10b83a 100644
--- a/app/assets/stylesheets/page_bundles/ci_status.scss
+++ b/app/assets/stylesheets/page_bundles/ci_status.scss
@@ -1,10 +1,7 @@
@import 'mixins_and_variables_and_functions';
.ci-status {
- padding: 2px 7px 4px;
border: 1px solid var(--border-color, $border-color);
- white-space: nowrap;
- border-radius: 4px;
&:hover,
&:focus {
diff --git a/app/assets/stylesheets/page_bundles/editor.scss b/app/assets/stylesheets/page_bundles/editor.scss
index b7b698b2128..36da979ba1f 100644
--- a/app/assets/stylesheets/page_bundles/editor.scss
+++ b/app/assets/stylesheets/page_bundles/editor.scss
@@ -163,7 +163,6 @@
.gitignore-selector,
.gitlab-ci-yml-selector,
.dockerfile-selector,
- .template-type-selector,
.metrics-dashboard-selector {
display: inline-block;
vertical-align: top;
diff --git a/app/assets/stylesheets/page_bundles/import.scss b/app/assets/stylesheets/page_bundles/import.scss
index cd5e6d32e4e..a6c08e344f9 100644
--- a/app/assets/stylesheets/page_bundles/import.scss
+++ b/app/assets/stylesheets/page_bundles/import.scss
@@ -1,48 +1,10 @@
@import 'mixins_and_variables_and_functions';
-.import-jobs-from-col {
- width: 37%;
-}
-
-
-.import-jobs-to-col {
- width: 37%;
-}
-
-.import-jobs-status-col {
- width: 25%;
-}
-
-.import-jobs-cta-col {
- width: 1%;
-}
-
-.import-entities-target-select {
- &.disabled {
- .import-entities-target-select-separator {
- color: var(--gray-400, $gray-400);
- border-color: var(--gray-100, $gray-100);
- background-color: var(--gray-10, $gray-10);
- }
- }
-
- .import-entities-target-select-separator {
- border-color: var(--gray-200, $gray-200);
- background-color: var(--gray-10, $gray-10);
- }
-
- .gl-form-input {
- box-shadow: inset 0 0 0 1px var(--gray-200, $gray-200);
- }
-}
-
$import-bar-height: $gl-spacing-scale-11;
.import-table-bar {
- @include gl-sticky;
height: $import-bar-height;
top: $header-height;
- z-index: 3;
html.with-performance-bar & {
top: calc(#{$header-height} + #{$performance-bar-height});
@@ -50,16 +12,11 @@ $import-bar-height: $gl-spacing-scale-11;
}
.import-table {
- border-collapse: separate;
-
thead {
- @include gl-sticky;
- background-color: var(--gray-10, $gray-10);
top: calc(#{$header-height} + #{$import-bar-height});
- z-index: 3;
html.with-performance-bar & {
- top: calc(#{$header-height + $performance-bar-height} + #{$import-bar-height});
+ top: calc(#{$header-height} + #{$performance-bar-height} + #{$import-bar-height});
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/members.scss b/app/assets/stylesheets/page_bundles/members.scss
index 8d2c0a8ca22..826921be8f0 100644
--- a/app/assets/stylesheets/page_bundles/members.scss
+++ b/app/assets/stylesheets/page_bundles/members.scss
@@ -76,6 +76,10 @@
width: px-to-rem(200px);
}
+ .col-activity {
+ width: px-to-rem(250px);
+ }
+
.col-actions {
width: px-to-rem(65px);
}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 4950561bcb7..5c699dd81df 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -1002,12 +1002,12 @@ $tabs-holder-z-index: 250;
.md-header {
.gl-tab-nav-item {
- @include gl-text-gray-900;
+ color: var(--gl-text-color, $gl-text-color);
@include gl-pb-5;
&:hover {
@include gl-bg-none;
- @include gl-text-gray-900;
+ color: var(--gl-text-color, $gl-text-color);
&:not(.gl-tab-nav-item-active) {
@include gl-inset-border-b-2-gray-200;
@@ -1017,7 +1017,7 @@ $tabs-holder-z-index: 250;
.gl-tab-nav-item-active {
@include gl-font-weight-bold;
- @include gl-text-gray-900;
+ color: var(--gl-text-color, $gl-text-color);
@include gl-inset-border-b-2-theme-accent;
&:active,
@@ -1197,13 +1197,13 @@ $tabs-holder-z-index: 250;
}
.mr-section-container {
+ .media-body {
+ column-gap: 0;
+ }
+
.state-container-action-buttons {
@include media-breakpoint-up(md) {
flex-direction: row-reverse;
-
- .btn {
- margin-left: auto;
- }
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index b995724ec7c..f08d6e3ca95 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -5,14 +5,14 @@
}
.timezone-dropdown {
- .dropdown-menu {
- @include gl-w-full;
- }
-
.gl-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
+
+ .btn-block {
+ margin-bottom: 0;
+ }
}
.modal-footer {
@@ -20,7 +20,7 @@
}
.invalid-dropdown {
- .gl-dropdown-toggle {
+ .gl-button.gl-dropdown-toggle {
@include inset-border-1-red-500;
&:hover {
diff --git a/app/assets/stylesheets/page_bundles/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss
index b35f5b38740..d4b0b4169d3 100644
--- a/app/assets/stylesheets/page_bundles/todos.scss
+++ b/app/assets/stylesheets/page_bundles/todos.scss
@@ -79,11 +79,11 @@
@include gl-py-0;
@include gl-px-1;
@include gl-m-0;
- @include gl-bg-gray-50;
@include gl-border-0;
@include gl-rounded-base;
@include gl-display-inline-flex;
- @include gl-text-body;
+ background: var(--gray-50, $gray-50);
+ color: var(--gl-text-color, $gl-text-color);
}
.gl-label-scoped {
diff --git a/app/assets/stylesheets/pages/ml_experiment_tracking.scss b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
index c1582f2090b..3c025b5d23f 100644
--- a/app/assets/stylesheets/pages/ml_experiment_tracking.scss
+++ b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
@@ -15,6 +15,20 @@
}
}
+table.ml-candidate-table {
+ table-layout: fixed;
+
+ tr td,
+ tr th {
+ padding: $gl-padding-8;
+
+ > * {
+ @include gl-display-block;
+ @include gl-text-truncate;
+ }
+ }
+}
+
table.candidate-details {
td {
padding: $gl-spacing-scale-3;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 89be1c024db..d26e29c4047 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -63,18 +63,17 @@
}
}
+ @include media-breakpoint-down(md) {
+ .time-ago {
+ align-items: flex-end;
+ }
+ }
+
.duration,
.finished-at {
color: $gl-text-color-secondary;
margin: 0;
white-space: nowrap;
-
- svg {
- width: 12px;
- height: 12px;
- vertical-align: middle;
- margin-right: 4px;
- }
}
.build-link a {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 15a32ea8ad3..ee91d955019 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -122,7 +122,7 @@
padding: 0;
background: transparent;
border: 0;
- line-height: 34px;
+ line-height: $gl-line-height-32;
margin: 0;
a {
@@ -495,7 +495,7 @@
.protected-branches-list,
.protected-tags-list {
- margin-bottom: 30px;
+ margin-bottom: 32px;
.settings-message {
margin: 0;
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index c7e55289b11..bb83a91bc57 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -536,10 +536,11 @@ a.gl-badge.badge-warning:active {
color: #89888d;
}
.gl-search-box-by-type-search-icon {
- margin: 0.5rem;
color: #89888d;
width: 1rem;
position: absolute;
+ left: 0.5rem;
+ top: calc(50% - 16px / 2);
}
.gl-search-box-by-type {
display: flex;
@@ -591,7 +592,7 @@ svg {
}
.toggle-sidebar-button .collapse-text,
.toggle-sidebar-button .icon-chevron-double-lg-left {
- color: #89888d;
+ color: #bfbfc3;
}
html {
overflow-y: scroll;
@@ -1136,15 +1137,13 @@ kbd {
}
.nav-sidebar li > a,
.nav-sidebar li > .fly-out-top-item-container {
+ height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
display: flex;
align-items: center;
border-radius: 0.25rem;
width: auto;
- line-height: 1rem;
margin: 1px 8px;
}
.nav-sidebar li.active > a {
@@ -1373,19 +1372,18 @@ kbd {
margin-top: 0.25rem;
}
.nav-sidebar-inner-scroll > div.context-header a {
+ height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
display: flex;
align-items: center;
border-radius: 0.25rem;
width: auto;
- line-height: 1rem;
margin: 1px 8px;
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0.125rem;
+ height: auto;
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
@@ -1398,6 +1396,7 @@ kbd {
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0.125rem;
+ height: auto;
}
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
@@ -1428,7 +1427,7 @@ kbd {
padding: 0 16px;
background-color: #24232a;
border: 0;
- color: #89888d;
+ color: #bfbfc3;
display: flex;
align-items: center;
background-color: #1f1e24;
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index f24b6fb9e81..9e1c6b065a0 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -536,10 +536,11 @@ a.gl-badge.badge-warning:active {
color: #737278;
}
.gl-search-box-by-type-search-icon {
- margin: 0.5rem;
color: #737278;
width: 1rem;
position: absolute;
+ left: 0.5rem;
+ top: calc(50% - 16px / 2);
}
.gl-search-box-by-type {
display: flex;
@@ -1136,15 +1137,13 @@ kbd {
}
.nav-sidebar li > a,
.nav-sidebar li > .fly-out-top-item-container {
+ height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
display: flex;
align-items: center;
border-radius: 0.25rem;
width: auto;
- line-height: 1rem;
margin: 1px 8px;
}
.nav-sidebar li.active > a {
@@ -1373,19 +1372,18 @@ kbd {
margin-top: 0.25rem;
}
.nav-sidebar-inner-scroll > div.context-header a {
+ height: 2rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
- padding-top: 0.5rem;
- padding-bottom: 0.5rem;
display: flex;
align-items: center;
border-radius: 0.25rem;
width: auto;
- line-height: 1rem;
margin: 1px 8px;
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0.125rem;
+ height: auto;
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
@@ -1398,6 +1396,7 @@ kbd {
padding: 0.25rem;
margin-bottom: 0.25rem;
margin-top: 0.125rem;
+ height: auto;
}
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 8db91fd9908..c471d6183d8 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -115,6 +115,8 @@ $data-viz-blue-950: #e9ebff;
$border-white-normal: $border-color;
+$gl-text-color-secondary: $gray-700;
+
$body-bg: $gray-10;
$input-bg: $white;
$input-focus-bg: $white;
@@ -130,7 +132,7 @@ $popover-color: $gray-950;
$popover-box-shadow: 0 2px 3px 1px $gray-700;
$popover-arrow-outer-color: $gray-800;
-$secondary: $gray-600;
+$secondary: $gray-700;
$yiq-text-dark: $gray-50;
$yiq-text-light: $gray-950;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 714dd932147..7d98a780e55 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -236,13 +236,44 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
-// TODO: Remove once https: //gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3198 is merged
-.gl-sm-ml-5 {
- @include gl-media-breakpoint-up(sm) {
- @include gl-ml-5;
+.gl-mt-n5 {
+ margin-top: -$gl-spacing-scale-5;
+}
+
+// Utils below are very specific so cannot be part of GitLab UI
+.gl-md-mt-5 {
+ @include gl-media-breakpoint-up(md) {
+ margin-top: $gl-spacing-scale-5;
+ }
+}
+
+.gl-sm-mr-0\! {
+ @include gl-media-breakpoint-down(md) {
+ margin-right: 0 !important;
+ }
+}
+
+.gl-sm-mb-5 {
+ @include gl-media-breakpoint-down(md) {
+ margin-bottom: $gl-spacing-scale-5;
+ }
+}
+
+.gl-md-mb-3\! {
+ @include gl-media-breakpoint-up(md) {
+ margin-bottom: $gl-spacing-scale-3 !important;
}
}
+
+.gl-gap-2 {
+ gap: $gl-spacing-scale-2;
+}
+
+.gl-hover-bg-t-gray-a-08:hover {
+ background-color: $t-gray-a-08;
+}
+
/* End gitlab-ui#1709 */
/*
@@ -263,3 +294,7 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
.gl-max-w-0 {
max-width: 0;
}
+
+.gl-isolate {
+ isolation: isolate;
+}