summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 09:40:42 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-10-20 09:40:42 +0000
commitee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch)
treef8479f94a28f66654c6a4f6fb99bad6b4e86a40e /app/assets
parent62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff)
downloadgitlab-ce-ee664acb356f8123f4f6b00b73c1e1cf0866c7fb.tar.gz
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue26
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue14
-rw-r--r--app/assets/javascripts/access_tokens/components/tokens_app.vue2
-rw-r--r--app/assets/javascripts/access_tokens/index.js11
-rw-r--r--app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js6
-rw-r--r--app/assets/javascripts/admin/background_migrations/components/database_listbox.vue2
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/base.vue112
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue113
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/index.js21
-rw-r--r--app/assets/javascripts/admin/deploy_keys/components/table.vue4
-rw-r--r--app/assets/javascripts/admin/statistics_panel/store/actions.js4
-rw-r--r--app/assets/javascripts/admin/users/components/users_table.vue4
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue7
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue42
-rw-r--r--app/assets/javascripts/alerts_settings/utils/cache_updates.js4
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue4
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue4
-rw-r--r--app/assets/javascripts/api/projects_api.js8
-rw-r--r--app/assets/javascripts/api/user_api.js4
-rw-r--r--app/assets/javascripts/awards_handler.js4
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue14
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue8
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js12
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js19
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js4
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js14
-rw-r--r--app/assets/javascripts/blame/blame_redirect.js23
-rw-r--r--app/assets/javascripts/blob/3d_viewer/index.js2
-rw-r--r--app/assets/javascripts/blob/3d_viewer/mesh_object.js2
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js1
-rw-r--r--app/assets/javascripts/blob/components/table_contents.vue14
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js4
-rw-r--r--app/assets/javascripts/blob/openapi/index.js4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js6
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js6
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue14
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue7
-rw-r--r--app/assets/javascripts/boards/constants.js11
-rw-r--r--app/assets/javascripts/boards/index.js1
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js4
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue29
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue58
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue55
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue62
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue3
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql8
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql8
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql8
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/settings.js (renamed from app/assets/javascripts/ci_variable_list/graphql/resolvers.js)113
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js8
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js12
-rw-r--r--app/assets/javascripts/ci_variable_list/utils.js10
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_events_list.vue6
-rw-r--r--app/assets/javascripts/clusters/agents/components/activity_history_item.vue10
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js4
-rw-r--r--app/assets/javascripts/code_navigation/utils/dom_utils.js14
-rw-r--r--app/assets/javascripts/code_navigation/utils/index.js8
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue11
-rw-r--r--app/assets/javascripts/commit_merge_requests.js4
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue20
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue264
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/label.vue34
-rw-r--r--app/assets/javascripts/content_editor/constants/index.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/diagram.js5
-rw-r--r--app/assets/javascripts/content_editor/extensions/external_keydown_handler.js38
-rw-r--r--app/assets/javascripts/content_editor/extensions/heading.js16
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js14
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference_label.js35
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js227
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js6
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js7
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js5
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue2
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js4
-rw-r--r--app/assets/javascripts/crm/contacts/components/contacts_root.vue11
-rw-r--r--app/assets/javascripts/crm/organizations/components/organizations_root.vue9
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js6
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue2
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js8
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutations.js4
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue8
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue332
-rw-r--r--app/assets/javascripts/deploy_tokens/index.js33
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue71
-rw-r--r--app/assets/javascripts/design_management/mixins/all_designs.js8
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue10
-rw-r--r--app/assets/javascripts/design_management/utils/cache_update.js6
-rw-r--r--app/assets/javascripts/diff.js4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue10
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue4
-rw-r--r--app/assets/javascripts/diffs/components/commit_widget.vue2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js6
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue18
-rw-r--r--app/assets/javascripts/diffs/mixins/draft_comments.js2
-rw-r--r--app/assets/javascripts/diffs/store/actions.js11
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js4
-rw-r--r--app/assets/javascripts/editor/schema/ci.json624
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue6
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue4
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue4
-rw-r--r--app/assets/javascripts/environments/components/empty_state.vue45
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_modal.vue160
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.vue16
-rw-r--r--app/assets/javascripts/environments/components/environment_folder.vue10
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue60
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue37
-rw-r--r--app/assets/javascripts/environments/components/new_environment.vue4
-rw-r--r--app/assets/javascripts/environments/constants.js31
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql4
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js8
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js8
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue8
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/details/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking/store/list/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/feature_flags/components/environments_dropdown.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue4
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/actions.js4
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js4
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js6
-rw-r--r--app/assets/javascripts/flash.js2
-rw-r--r--app/assets/javascripts/gpg_badges.js4
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js4
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js52
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json2
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search.query.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql6
-rw-r--r--app/assets/javascripts/group.js6
-rw-r--r--app/assets/javascripts/groups/components/app.vue28
-rw-r--r--app/assets/javascripts/groups/components/groups.vue20
-rw-r--r--app/assets/javascripts/groups/components/new_top_level_group_alert.vue40
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue177
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue2
-rw-r--r--app/assets/javascripts/groups/constants.js25
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/settings/components/access_dropdown.vue4
-rw-r--r--app/assets/javascripts/groups_select.js12
-rw-r--r--app/assets/javascripts/header_search/components/app.vue19
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue7
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue23
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue4
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue6
-rw-r--r--app/assets/javascripts/ide/index.js4
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js6
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js6
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js4
-rw-r--r--app/assets/javascripts/ide/utils.js22
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue28
-rw-r--r--app/assets/javascripts/import_entities/import_groups/services/status_poller.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue51
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue30
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue6
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js1
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js29
-rw-r--r--app/assets/javascripts/integrations/constants.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue87
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js56
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue)7
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue51
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/constants.js22
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js25
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js2
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js28
-rw-r--r--app/assets/javascripts/issuable/issuable_context.js19
-rw-r--r--app/assets/javascripts/issuable/issuable_form.js52
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue7
-rw-r--r--app/assets/javascripts/issues/list/constants.js10
-rw-r--r--app/assets/javascripts/issues/show/components/edited.vue33
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue31
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue79
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue7
-rw-r--r--app/assets/javascripts/issues/show/index.js1
-rw-r--r--app/assets/javascripts/jobs/components/table/constants.js19
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue4
-rw-r--r--app/assets/javascripts/jobs/store/actions.js12
-rw-r--r--app/assets/javascripts/labels/components/promote_label_modal.vue4
-rw-r--r--app/assets/javascripts/labels/group_label_subscription.js6
-rw-r--r--app/assets/javascripts/labels/label_manager.js6
-rw-r--r--app/assets/javascripts/labels/labels_select.js6
-rw-r--r--app/assets/javascripts/labels/project_label_subscription.js4
-rw-r--r--app/assets/javascripts/lib/dompurify.js13
-rw-r--r--app/assets/javascripts/lib/utils/autosave.js40
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js51
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js18
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js26
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js61
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js14
-rw-r--r--app/assets/javascripts/listbox/index.js48
-rw-r--r--app/assets/javascripts/logo.js2
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue6
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue5
-rw-r--r--app/assets/javascripts/members/utils.js5
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue4
-rw-r--r--app/assets/javascripts/merge_conflicts/store/actions.js4
-rw-r--r--app/assets/javascripts/merge_request.js6
-rw-r--r--app/assets/javascripts/merge_request_tabs.js6
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue1
-rw-r--r--app/assets/javascripts/milestones/components/promote_milestone_modal.vue4
-rw-r--r--app/assets/javascripts/milestones/milestone.js4
-rw-r--r--app/assets/javascripts/milestones/milestone_select.js2
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js4
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue8
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js30
-rw-r--r--app/assets/javascripts/mr_notes/index.js3
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js13
-rw-r--r--app/assets/javascripts/namespaces/leave_by_url.js4
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue10
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue2
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue9
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue19
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter_note.vue2
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue5
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue67
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue4
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue19
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue38
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue24
-rw-r--r--app/assets/javascripts/notes/components/timeline_toggle.vue1
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue4
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js34
-rw-r--r--app/assets/javascripts/notes/i18n.js2
-rw-r--r--app/assets/javascripts/notes/index.js13
-rw-r--r--app/assets/javascripts/notes/mixins/diff_line_note_form.js6
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js156
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js4
-rw-r--r--app/assets/javascripts/notes/timeline.js16
-rw-r--r--app/assets/javascripts/notes/utils/get_notes_filter_data.js21
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js10
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js12
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue18
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue11
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js11
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue60
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_downloader.js4
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js4
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js4
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index.js9
-rw-r--r--app/assets/javascripts/pages/admin/dashboard/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/groups/show/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js6
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js4
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js9
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/settings/index.js2
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue4
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue95
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js20
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue4
-rw-r--r--app/assets/javascripts/pages/profiles/index.js2
-rw-r--r--app/assets/javascripts/pages/profiles/init_timezone_dropdown.js34
-rw-r--r--app/assets/javascripts/pages/projects/blame/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue4
-rw-r--r--app/assets/javascripts/pages/projects/hooks/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js15
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js3
-rw-r--r--app/assets/javascripts/pages/projects/project.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue30
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue221
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js6
-rw-r--r--app/assets/javascripts/pdf/index.vue17
-rw-r--r--app/assets/javascripts/persistent_user_callout.js6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue48
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js3
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue2
-rw-r--r--app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue2
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue254
-rw-r--r--app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql9
-rw-r--r--app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql11
-rw-r--r--app/assets/javascripts/pipeline_new/graphql/resolvers.js29
-rw-r--r--app/assets/javascripts/pipeline_new/index.js14
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue134
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue18
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue66
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue32
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue32
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue29
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue36
-rw-r--r--app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue95
-rw-r--r--app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql6
-rw-r--r--app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql40
-rw-r--r--app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js32
-rw-r--r--app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js32
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/widgets/list.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/jobs_app.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue11
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue4
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js4
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js16
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js4
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js4
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue6
-rw-r--r--app/assets/javascripts/profile/gl_crop.js1
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue16
-rw-r--r--app/assets/javascripts/profile/profile.js14
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/projects/commit/store/actions.js4
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue8
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue4
-rw-r--r--app/assets/javascripts/projects/commits/store/actions.js4
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue46
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown.vue6
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue4
-rw-r--r--app/assets/javascripts/projects/compare/index.js3
-rw-r--r--app/assets/javascripts/projects/project_find_file.js4
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js6
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue)2
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue (renamed from app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue)0
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js42
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue207
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue99
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue110
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js11
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql50
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue6
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue38
-rw-r--r--app/assets/javascripts/projects/settings/components/transfer_project_form.vue170
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/init_transfer_project_form.js2
-rw-r--r--app/assets/javascripts/projects/settings/mount_default_branch_selector.js22
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue9
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue37
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js12
-rw-r--r--app/assets/javascripts/projects/star.js6
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue4
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js6
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js6
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js4
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue174
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue4
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_root.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue4
-rw-r--r--app/assets/javascripts/releases/components/app_show.vue4
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue92
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js17
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js1
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/mutations.js4
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/state.js2
-rw-r--r--app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue61
-rw-r--r--app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue65
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/actions.js76
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/getters.js45
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/index.js17
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/mutations.js20
-rw-r--r--app/assets/javascripts/reports/accessibility_report/store/state.js28
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js3
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue35
-rw-r--r--app/assets/javascripts/repository/commits_service.js4
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue4
-rw-r--r--app/assets/javascripts/repository/components/blob_controls.vue34
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue4
-rw-r--r--app/assets/javascripts/repository/components/new_directory_modal.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue6
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue4
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue38
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue7
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_owner_cell.vue63
-rw-r--r--app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue16
-rw-r--r--app/assets/javascripts/runner/components/runner_delete_button.vue32
-rw-r--r--app/assets/javascripts/runner/components/runner_details.vue7
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue1
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue44
-rw-r--r--app/assets/javascripts/runner/components/runner_list_empty_state.vue9
-rw-r--r--app/assets/javascripts/runner/components/runner_membership_toggle.vue42
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue18
-rw-r--r--app/assets/javascripts/runner/constants.js19
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners.query.graphql5
-rw-r--r--app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql3
-rw-r--r--app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql14
-rw-r--r--app/assets/javascripts/runner/graphql/list/local_state.js22
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue50
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js6
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js18
-rw-r--r--app/assets/javascripts/search/index.js10
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue25
-rw-r--r--app/assets/javascripts/search/sidebar/index.js9
-rw-r--r--app/assets/javascripts/search/store/actions.js10
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue47
-rw-r--r--app/assets/javascripts/search_settings/components/search_settings.vue41
-rw-r--r--app/assets/javascripts/search_settings/mount.js1
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue11
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade_banner.vue2
-rw-r--r--app/assets/javascripts/service_ping_consent.js4
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue10
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue15
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/constants.js7
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js6
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_participants.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql9
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js33
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js8
-rw-r--r--app/assets/javascripts/single_file_diff.js4
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue8
-rw-r--r--app/assets/javascripts/task_list.js4
-rw-r--r--app/assets/javascripts/token_access/components/token_access.vue32
-rw-r--r--app/assets/javascripts/users_select/index.js31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue314
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue137
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue119
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue98
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue67
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue68
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js13
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue40
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_badge_link.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/confidentiality_badge.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/gitlab_version_check.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/utils.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_epics.query.graphql (renamed from app/assets/javascripts/boards/graphql/board_blocking_epics.query.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_issues.query.graphql (renamed from app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql)2
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue (renamed from app/assets/javascripts/boards/components/board_blocked_icon.vue)21
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue216
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/store/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue (renamed from app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue)16
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/history_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js9
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js45
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js41
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js15
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue88
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue119
-rw-r--r--app/assets/javascripts/vue_shared/components/url_sync.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue71
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue70
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue122
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue117
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue1
-rw-r--r--app/assets/javascripts/vue_shared/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/directives/safe_html.js25
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue11
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue4
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_app.vue134
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_mask_item.vue90
-rw-r--r--app/assets/javascripts/webhooks/index.js25
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue19
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue151
-rw-r--r--app/assets/javascripts/work_items/components/work_item_due_date.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue118
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue32
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue248
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js15
-rw-r--r--app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql16
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.query.graphql9
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql21
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql19
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql16
-rw-r--r--app/assets/javascripts/work_items/index.js10
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue43
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss7
-rw-r--r--app/assets/stylesheets/bootstrap_migration_reset.scss4
-rw-r--r--app/assets/stylesheets/components/batch_comments/review_bar.scss71
-rw-r--r--app/assets/stylesheets/components/date_time_picker.scss5
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss193
-rw-r--r--app/assets/stylesheets/components/design_management/design_list_item.scss19
-rw-r--r--app/assets/stylesheets/components/feature_highlight.scss5
-rw-r--r--app/assets/stylesheets/components/milestone_combobox.scss5
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss41
-rw-r--r--app/assets/stylesheets/components/release_block.scss3
-rw-r--r--app/assets/stylesheets/components/shortcuts_help.scss (renamed from app/assets/stylesheets/pages/help.scss)11
-rw-r--r--app/assets/stylesheets/framework.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss1
-rw-r--r--app/assets/stylesheets/framework/calendar.scss8
-rw-r--r--app/assets/stylesheets/framework/ci_variable_list.scss77
-rw-r--r--app/assets/stylesheets/framework/diffs.scss247
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss1
-rw-r--r--app/assets/stylesheets/framework/flash.scss27
-rw-r--r--app/assets/stylesheets/framework/flex_grid.scss52
-rw-r--r--app/assets/stylesheets/framework/gfm.scss6
-rw-r--r--app/assets/stylesheets/framework/lists.scss7
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss29
-rw-r--r--app/assets/stylesheets/framework/memory_graph.scss4
-rw-r--r--app/assets/stylesheets/framework/tables.scss16
-rw-r--r--app/assets/stylesheets/framework/timeline.scss26
-rw-r--r--app/assets/stylesheets/framework/toggle.scss131
-rw-r--r--app/assets/stylesheets/framework/typography.scss12
-rw-r--r--app/assets/stylesheets/framework/variables.scss21
-rw-r--r--app/assets/stylesheets/framework/wells.scss9
-rw-r--r--app/assets/stylesheets/highlight/themes/dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/monokai.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss4
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-dark.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss4
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss2
-rw-r--r--app/assets/stylesheets/lazy_bundles/gridstack.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/admin/geo_nodes.scss45
-rw-r--r--app/assets/stylesheets/page_bundles/admin/geo_replicable.scss18
-rw-r--r--app/assets/stylesheets/page_bundles/cluster_agents.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/clusters.scss (renamed from app/assets/stylesheets/pages/clusters.scss)14
-rw-r--r--app/assets/stylesheets/page_bundles/graph_charts.scss27
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss7
-rw-r--r--app/assets/stylesheets/page_bundles/incidents.scss73
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss214
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss323
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss35
-rw-r--r--app/assets/stylesheets/page_bundles/operations.scss (renamed from app/assets/stylesheets/components/dashboard_skeleton.scss)24
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline_schedules.scss96
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss58
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/prometheus.scss (renamed from app/assets/stylesheets/pages/prometheus.scss)17
-rw-r--r--app/assets/stylesheets/page_bundles/releases.scss (renamed from app/assets/stylesheets/components/release_block_milestone_info.scss)6
-rw-r--r--app/assets/stylesheets/page_bundles/tree.scss (renamed from app/assets/stylesheets/pages/tree.scss)69
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss19
-rw-r--r--app/assets/stylesheets/pages/deploy_keys.scss4
-rw-r--r--app/assets/stylesheets/pages/environment_logs.scss54
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss126
-rw-r--r--app/assets/stylesheets/pages/issues.scss5
-rw-r--r--app/assets/stylesheets/pages/notes.scss336
-rw-r--r--app/assets/stylesheets/pages/profile.scss105
-rw-r--r--app/assets/stylesheets/pages/projects.scss63
-rw-r--r--app/assets/stylesheets/pages/search.scss19
-rw-r--r--app/assets/stylesheets/pages/service_desk.scss7
-rw-r--r--app/assets/stylesheets/pages/settings.scss25
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss27
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss22
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss9
-rw-r--r--app/assets/stylesheets/themes/_dark.scss10
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss5
-rw-r--r--app/assets/stylesheets/utilities.scss131
692 files changed, 10410 insertions, 6382 deletions
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
index 461b2dad479..57a237c3e84 100644
--- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -45,16 +45,34 @@ export default {
'initialActiveAccessTokens',
'noActiveTokensMessage',
'showRole',
+ 'information',
],
data() {
return {
- activeAccessTokens: this.initialActiveAccessTokens,
+ activeAccessTokens: convertObjectPropsToCamelCase(this.initialActiveAccessTokens, {
+ deep: true,
+ }),
currentPage: INITIAL_PAGE,
};
},
computed: {
filteredFields() {
- return this.showRole ? FIELDS : FIELDS.filter((field) => field.key !== 'role');
+ const ignoredFields = [];
+
+ // Show 'action' column only when there are no active tokens or when some of them have a revokePath
+ const showAction =
+ this.activeAccessTokens.length === 0 ||
+ this.activeAccessTokens.some((token) => token.revokePath);
+
+ if (!showAction) {
+ ignoredFields.push('action');
+ }
+
+ if (!this.showRole) {
+ ignoredFields.push('role');
+ }
+
+ return FIELDS.filter(({ key }) => !ignoredFields.includes(key));
},
header() {
return sprintf(this.$options.i18n.header, {
@@ -100,6 +118,10 @@ export default {
<hr />
<h5>{{ header }}</h5>
+ <p v-if="information" data-testid="information-section">
+ {{ information }}
+ </p>
+
<gl-table
data-testid="active-tokens"
:empty-text="noActiveTokensMessage"
diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
index 6b52bd84656..ce5342ad1ea 100644
--- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
+++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
@@ -2,10 +2,13 @@
import { GlAlert } from '@gitlab/ui';
import { createAlert, VARIANT_INFO } from '~/flash';
import { __, n__, sprintf } from '~/locale';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
import { EVENT_ERROR, EVENT_SUCCESS, FORM_SELECTOR } from './constants';
+const convertEventDetail = (event) => convertObjectPropsToCamelCase(event.detail, { deep: true });
+
export default {
EVENT_ERROR,
EVENT_SUCCESS,
@@ -54,8 +57,8 @@ export default {
/** @type {HTMLFormElement} */
this.form = document.querySelector(FORM_SELECTOR);
- /** @type {HTMLInputElement} */
- this.submitButton = this.form.querySelector('input[type=submit]');
+ /** @type {HTMLButtonElement} */
+ this.submitButton = this.form.querySelector('[type=submit]');
},
methods: {
beforeDisplayResults() {
@@ -68,20 +71,21 @@ export default {
onError(event) {
this.beforeDisplayResults();
- const [{ errors }] = event.detail;
+ const [{ errors }] = convertEventDetail(event);
this.errors = errors;
this.submitButton.classList.remove('disabled');
+ this.submitButton.removeAttribute('disabled');
},
onSuccess(event) {
this.beforeDisplayResults();
- const [{ new_token: newToken }] = event.detail;
+ const [{ newToken }] = convertEventDetail(event);
this.newToken = newToken;
this.infoAlert = createAlert({ message: this.alertInfoMessage, variant: VARIANT_INFO });
- // Selectively reset all input fields except for the date picker and submit.
+ // Selectively reset all input fields except for the date picker.
// The form token creation is not controlled by Vue.
this.form.querySelectorAll('input[type=text]:not([id$=expires_at])').forEach((el) => {
el.value = '';
diff --git a/app/assets/javascripts/access_tokens/components/tokens_app.vue b/app/assets/javascripts/access_tokens/components/tokens_app.vue
index 10d4d62d803..1f72f5e19e2 100644
--- a/app/assets/javascripts/access_tokens/components/tokens_app.vue
+++ b/app/assets/javascripts/access_tokens/components/tokens_app.vue
@@ -79,7 +79,7 @@ export default {
</script>
<template>
- <div>
+ <div class="js-search-settings-section">
<token
v-for="(tokenData, tokenType) in enabledTokenTypes"
:key="tokenType"
diff --git a/app/assets/javascripts/access_tokens/index.js b/app/assets/javascripts/access_tokens/index.js
index f0c1b415157..510f118bbb5 100644
--- a/app/assets/javascripts/access_tokens/index.js
+++ b/app/assets/javascripts/access_tokens/index.js
@@ -20,6 +20,7 @@ export const initAccessTokenTableApp = () => {
const {
accessTokenType,
accessTokenTypePlural,
+ information,
initialActiveAccessTokens: initialActiveAccessTokensJson,
noActiveTokensMessage: noTokensMessage,
} = el.dataset;
@@ -30,12 +31,7 @@ export const initAccessTokenTableApp = () => {
sprintf(__('This user has no active %{accessTokenTypePlural}.'), { accessTokenTypePlural });
const showRole = 'showRole' in el.dataset;
- const initialActiveAccessTokens = convertObjectPropsToCamelCase(
- JSON.parse(initialActiveAccessTokensJson),
- {
- deep: true,
- },
- );
+ const initialActiveAccessTokens = JSON.parse(initialActiveAccessTokensJson);
return new Vue({
el,
@@ -43,6 +39,7 @@ export const initAccessTokenTableApp = () => {
provide: {
accessTokenType,
accessTokenTypePlural,
+ information,
initialActiveAccessTokens,
noActiveTokensMessage,
showRole,
@@ -103,7 +100,7 @@ export const initNewAccessTokenApp = () => {
export const initTokensApp = () => {
const el = document.getElementById('js-tokens-app');
- if (!el) return false;
+ if (!el) return null;
const tokensData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.tokensData), {
deep: true,
diff --git a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
index 8ad218ab97b..a41ff42df20 100644
--- a/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
+++ b/app/assets/javascripts/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue
@@ -2,7 +2,7 @@
import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf, GlBadge } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
@@ -193,7 +193,7 @@ export default {
window.location.reload();
}
if (!values[0] && !values[1]) {
- createFlash({
+ createAlert({
message: s__(
'ContextCommits|Failed to create/remove context commits. Please try again.',
),
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index 4e5a2c7b371..d4c9db2fa33 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,6 +1,6 @@
import _ from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -71,7 +71,7 @@ export const createContextCommits = ({ state }, { commits, forceReload = false }
})
.catch(() => {
if (forceReload) {
- createFlash({
+ createAlert({
message: s__('ContextCommits|Failed to create context commits. Please try again.'),
});
}
@@ -113,7 +113,7 @@ export const removeContextCommits = ({ state }, forceReload = false) =>
})
.catch(() => {
if (forceReload) {
- createFlash({
+ createAlert({
message: s__('ContextCommits|Failed to delete context commits. Please try again.'),
});
}
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 7f6e5dc4f35..80c216024a0 100644
--- a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
+++ b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
@@ -44,7 +44,7 @@ export default {
:items="databases"
right
:toggle-text="selectedDatabase"
- aria-labelledby="label"
+ toggle-aria-labelled-by="label"
@select="selectDatabase"
/>
</div>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
new file mode 100644
index 00000000000..b7bafe46327
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlPagination } from '@gitlab/ui';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
+import { createAlert, VARIANT_DANGER } from '~/flash';
+import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import MessagesTable from './messages_table.vue';
+
+const PER_PAGE = 20;
+
+export default {
+ name: 'BroadcastMessagesBase',
+ components: {
+ GlPagination,
+ MessagesTable,
+ },
+
+ props: {
+ page: {
+ type: Number,
+ required: true,
+ },
+ messagesCount: {
+ type: Number,
+ required: true,
+ },
+ messages: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ i18n: {
+ deleteError: s__(
+ 'BroadcastMessages|There was an issue deleting this message, please try again later.',
+ ),
+ },
+
+ data() {
+ return {
+ currentPage: this.page,
+ totalMessages: this.messagesCount,
+ visibleMessages: this.messages.map((message) => ({
+ ...message,
+ disable_delete: false,
+ })),
+ };
+ },
+
+ computed: {
+ hasVisibleMessages() {
+ return this.visibleMessages.length > 0;
+ },
+ },
+
+ watch: {
+ totalMessages(newVal, oldVal) {
+ // Pagination controls disappear when there is only
+ // one page worth of messages. Since we're relying on static data,
+ // this could hide messages on the next page, or leave the user
+ // stranded on page 2 when deleting the last message.
+ // Force a page reload to avoid this edge case.
+ if (newVal === PER_PAGE && oldVal === PER_PAGE + 1) {
+ redirectTo(this.buildPageUrl(1));
+ }
+ },
+ },
+
+ methods: {
+ buildPageUrl(newPage) {
+ return buildUrlWithCurrentLocation(`?page=${newPage}`);
+ },
+
+ async deleteMessage(messageId) {
+ const index = this.visibleMessages.findIndex((m) => m.id === messageId);
+ if (!index === -1) return;
+
+ const message = this.visibleMessages[index];
+ this.$set(this.visibleMessages, index, { ...message, disable_delete: true });
+
+ try {
+ await axios.delete(message.delete_path);
+ } catch (e) {
+ this.$set(this.visibleMessages, index, { ...message, disable_delete: false });
+ createAlert({ message: this.$options.i18n.deleteError, variant: VARIANT_DANGER });
+ return;
+ }
+
+ // Remove the message from the table
+ this.visibleMessages = this.visibleMessages.filter((m) => m.id !== messageId);
+ this.totalMessages -= 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <messages-table
+ v-if="hasVisibleMessages"
+ :messages="visibleMessages"
+ @delete-message="deleteMessage"
+ />
+ <gl-pagination
+ v-model="currentPage"
+ :total-items="totalMessages"
+ :link-gen="buildPageUrl"
+ align="center"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
new file mode 100644
index 00000000000..1408312d3e4
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
@@ -0,0 +1,113 @@
+<script>
+import { GlButton, GlTableLite, GlSafeHtmlDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!';
+
+export default {
+ name: 'MessagesTable',
+ components: {
+ GlButton,
+ GlTableLite,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ i18n: {
+ edit: __('Edit'),
+ delete: __('Delete'),
+ },
+ props: {
+ messages: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ fields() {
+ if (this.glFeatures.roleTargetedBroadcastMessages) return this.$options.allFields;
+ return this.$options.allFields.filter((f) => f.key !== 'target_roles');
+ },
+ },
+ allFields: [
+ {
+ key: 'status',
+ label: __('Status'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'preview',
+ label: __('Preview'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'starts_at',
+ label: __('Starts'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'ends_at',
+ label: __('Ends'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'target_roles',
+ label: __('Target roles'),
+ tdClass: DEFAULT_TD_CLASSES,
+ thAttr: { 'data-testid': 'target-roles-th' },
+ },
+ {
+ key: 'target_path',
+ label: __('Target Path'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'type',
+ label: __('Type'),
+ tdClass: DEFAULT_TD_CLASSES,
+ },
+ {
+ key: 'buttons',
+ label: '',
+ tdClass: `${DEFAULT_TD_CLASSES} gl-white-space-nowrap`,
+ },
+ ],
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'],
+ },
+};
+</script>
+<template>
+ <gl-table-lite
+ :items="messages"
+ :fields="fields"
+ :tbody-tr-attr="{ 'data-testid': 'message-row' }"
+ stacked="md"
+ >
+ <template #cell(preview)="{ item: { preview } }">
+ <div v-safe-html:[$options.safeHtmlConfig]="preview"></div>
+ </template>
+
+ <template #cell(buttons)="{ item: { id, edit_path, disable_delete } }">
+ <gl-button
+ icon="pencil"
+ :aria-label="$options.i18n.edit"
+ :href="edit_path"
+ data-testid="edit-message"
+ />
+
+ <gl-button
+ class="gl-ml-3"
+ icon="remove"
+ variant="danger"
+ :aria-label="$options.i18n.delete"
+ rel="nofollow"
+ :disabled="disable_delete"
+ :data-testid="`delete-message-${id}`"
+ @click="$emit('delete-message', id)"
+ />
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/index.js b/app/assets/javascripts/admin/broadcast_messages/index.js
new file mode 100644
index 00000000000..81952d2033e
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/index.js
@@ -0,0 +1,21 @@
+import Vue from 'vue';
+import BroadcastMessagesBase from './components/base.vue';
+
+export default () => {
+ const el = document.querySelector('#js-broadcast-messages');
+ const { page, messagesCount, messages } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'BroadcastMessages',
+ render(createElement) {
+ return createElement(BroadcastMessagesBase, {
+ props: {
+ page: Number(page),
+ messagesCount: Number(messagesCount),
+ messages: JSON.parse(messages),
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/deploy_keys/components/table.vue b/app/assets/javascripts/admin/deploy_keys/components/table.vue
index 6b140590938..be85ee43891 100644
--- a/app/assets/javascripts/admin/deploy_keys/components/table.vue
+++ b/app/assets/javascripts/admin/deploy_keys/components/table.vue
@@ -5,7 +5,7 @@ import { __ } from '~/locale';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import csrf from '~/lib/utils/csrf';
export default {
@@ -151,7 +151,7 @@ export default {
}),
);
} catch (error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.apiErrorMessage,
captureError: true,
error,
diff --git a/app/assets/javascripts/admin/statistics_panel/store/actions.js b/app/assets/javascripts/admin/statistics_panel/store/actions.js
index 77782cdc187..4f952698d7a 100644
--- a/app/assets/javascripts/admin/statistics_panel/store/actions.js
+++ b/app/assets/javascripts/admin/statistics_panel/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -21,7 +21,7 @@ export const receiveStatisticsSuccess = ({ commit }, statistics) =>
export const receiveStatisticsError = ({ commit }, error) => {
commit(types.RECEIVE_STATISTICS_ERROR, error);
- createFlash({
+ createAlert({
message: s__('AdminDashboard|Error loading the statistics. Please try again'),
});
};
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue
index b4b84594276..f569cda0a4b 100644
--- a/app/assets/javascripts/admin/users/components/users_table.vue
+++ b/app/assets/javascripts/admin/users/components/users_table.vue
@@ -1,6 +1,6 @@
<script>
import { GlSkeletonLoader, GlTable } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils';
import { thWidthPercent } from '~/lib/utils/table_utility';
import { s__, __ } from '~/locale';
@@ -50,7 +50,7 @@ export default {
}, {});
},
error(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.groupCountFetchError,
captureError: true,
error,
diff --git a/app/assets/javascripts/alert_management/components/alert_management_table.vue b/app/assets/javascripts/alert_management/components/alert_management_table.vue
index 37a6ea16018..c0cac958a42 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -216,8 +216,11 @@ export default {
this.pagination = initialPaginationState;
this.sort = sortObjectToString({ sortBy, sortDesc });
},
+ showAlertLink({ iid }) {
+ return joinPaths(window.location.pathname, iid, 'details');
+ },
navigateToAlertDetails({ iid }, index, { metaKey }) {
- return visitUrl(joinPaths(window.location.pathname, iid, 'details'), metaKey);
+ return visitUrl(this.showAlertLink({ iid }), metaKey);
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
@@ -357,7 +360,7 @@ export default {
:title="`${item.iid} - ${item.title}`"
data-testid="idField"
>
- #{{ item.iid }} {{ item.title }}
+ <gl-link :href="showAlertLink(item)"> #{{ item.iid }} {{ item.title }} </gl-link>
</div>
</template>
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 1f970ef1846..bf456b6adaa 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -2,7 +2,7 @@
import { GlButton, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import httpStatusCodes from '~/lib/utils/http_status';
import { typeSet, i18n, tabIndices } from '../constants';
@@ -75,7 +75,7 @@ export default {
return nodes;
},
error(err) {
- createFlash({ message: err });
+ createAlert({ message: err });
},
},
currentIntegration: {
@@ -125,7 +125,7 @@ export default {
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
return;
}
@@ -140,7 +140,7 @@ export default {
}
})
.catch(() => {
- createFlash({ message: ADD_INTEGRATION_ERROR });
+ createAlert({ message: ADD_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -161,7 +161,7 @@ export default {
.then(({ data: { httpIntegrationUpdate, prometheusIntegrationUpdate } = {} } = {}) => {
const error = httpIntegrationUpdate?.errors[0] || prometheusIntegrationUpdate?.errors[0];
if (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
return;
}
@@ -174,13 +174,13 @@ export default {
this.clearCurrentIntegration({ type });
}
- createFlash({
+ createAlert({
message: this.$options.i18n.changesSaved,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
})
.catch(() => {
- createFlash({ message: UPDATE_INTEGRATION_ERROR });
+ createAlert({ message: UPDATE_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -198,7 +198,7 @@ export default {
const [error] =
httpIntegrationResetToken?.errors || prometheusIntegrationResetToken.errors;
if (error) {
- return createFlash({ message: error });
+ return createAlert({ message: error });
}
const integration =
@@ -212,14 +212,14 @@ export default {
variables: integration,
});
- return createFlash({
+ return createAlert({
message: this.$options.i18n.changesSaved,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
},
)
.catch(() => {
- createFlash({ message: RESET_INTEGRATION_TOKEN_ERROR });
+ createAlert({ message: RESET_INTEGRATION_TOKEN_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -252,7 +252,7 @@ export default {
);
},
error() {
- createFlash({ message: DEFAULT_ERROR });
+ createAlert({ message: DEFAULT_ERROR });
},
});
} else {
@@ -272,7 +272,7 @@ export default {
this.tabIndex = tabIndex;
})
.catch(() => {
- createFlash({ message: DEFAULT_ERROR });
+ createAlert({ message: DEFAULT_ERROR });
});
},
deleteIntegration({ id, type }) {
@@ -290,16 +290,16 @@ export default {
.then(({ data: { httpIntegrationDestroy } = {} } = {}) => {
const error = httpIntegrationDestroy?.errors[0];
if (error) {
- return createFlash({ message: error });
+ return createAlert({ message: error });
}
this.clearCurrentIntegration({ type });
- return createFlash({
+ return createAlert({
message: this.$options.i18n.integrationRemoved,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
})
.catch(() => {
- createFlash({ message: DELETE_INTEGRATION_ERROR });
+ createAlert({ message: DELETE_INTEGRATION_ERROR });
})
.finally(() => {
this.isUpdating = false;
@@ -320,9 +320,9 @@ export default {
return service
.updateTestAlert(payload)
.then(() => {
- return createFlash({
+ return createAlert({
message: this.$options.i18n.alertSent,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
})
.catch((error) => {
@@ -330,7 +330,7 @@ export default {
if (error.response?.status === httpStatusCodes.FORBIDDEN) {
message = INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR;
}
- createFlash({ message });
+ createAlert({ message });
});
},
saveAndTestAlertPayload(integration, payload) {
diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
index a50b6515afa..2e64312b0e0 100644
--- a/app/assets/javascripts/alerts_settings/utils/cache_updates.js
+++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
@@ -1,5 +1,5 @@
import produce from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages';
@@ -59,7 +59,7 @@ const addIntegrationToStore = (
};
const onError = (data, message) => {
- createFlash({ message });
+ createAlert({ message });
throw new Error(data.errors);
};
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index 567e534d9cf..ffb61230661 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { flatten, isEqual, keyBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sprintf, s__ } from '~/locale';
import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils';
@@ -17,7 +17,7 @@ const requestData = ({ request, endpoint, path, params, name }) => {
),
{ requestTypeName: name },
);
- createFlash({ message });
+ createAlert({ message });
});
};
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
index 457a52d3807..5651789e2c7 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
@@ -1,7 +1,7 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { number } from '~/lib/utils/unit_format';
import { __, s__ } from '~/locale';
import usageTrendsCountQuery from '../graphql/queries/usage_trends_count.query.graphql';
@@ -35,7 +35,7 @@ export default {
});
},
error(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.loadCountsError,
captureError: true,
error,
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index 667aa878261..5b5abbdf50b 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -5,6 +5,7 @@ import { buildApiUrl } from './api_utils';
const PROJECTS_PATH = '/api/:version/projects.json';
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
const PROJECT_REPOSITORY_SIZE_PATH = '/api/:version/projects/:id/repository_size';
+const PROJECT_TRANSFER_LOCATIONS_PATH = 'api/:version/projects/:id/transfer_locations';
export function getProjects(query, options, callback = () => {}) {
const url = buildApiUrl(PROJECTS_PATH);
@@ -42,3 +43,10 @@ export function updateRepositorySize(projectPath) {
);
return axios.post(url);
}
+
+export const getTransferLocations = (projectId, params = {}) => {
+ const url = buildApiUrl(PROJECT_TRANSFER_LOCATIONS_PATH).replace(':id', projectId);
+ const defaultParams = { per_page: DEFAULT_PER_PAGE };
+
+ return axios.get(url, { params: { ...defaultParams, ...params } });
+};
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index c743b18d572..369abe95d49 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -1,5 +1,5 @@
import { DEFAULT_PER_PAGE } from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
@@ -55,7 +55,7 @@ export function getUserProjects(userId, query, options, callback) {
})
.then(({ data }) => callback(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching projects'),
}),
);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index a3ffb4df7b7..9ab1d6bfd80 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -7,7 +7,7 @@ import { getEmojiScoreWithIntent } from '~/emoji/utils';
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
import * as Emoji from '~/emoji';
import { dispose, fixTitle } from '~/tooltips';
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { __ } from './locale';
@@ -491,7 +491,7 @@ export class AwardsHandler {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
}),
);
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index d1570e16639..f68666f8a0c 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -2,7 +2,7 @@
import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { s__, sprintf } from '~/locale';
import createEmptyBadge from '../empty_badge';
import Badge from './badge.vue';
@@ -136,14 +136,14 @@ export default {
if (this.isEditing) {
return this.saveBadge()
.then(() => {
- createFlash({
+ createAlert({
message: s__('Badges|Badge saved.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
this.wasValidated = false;
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__(
'Badges|Saving the badge failed, please check the entered URLs and try again.',
),
@@ -154,14 +154,14 @@ export default {
return this.addBadge()
.then(() => {
- createFlash({
+ createAlert({
message: s__('Badges|New badge added.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
this.wasValidated = false;
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__(
'Badges|Adding the badge failed, please check the entered URLs and try again.',
),
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index 0303930de5d..a7a21d65475 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { __, s__ } from '~/locale';
import Badge from './badge.vue';
import BadgeForm from './badge_form.vue';
@@ -40,13 +40,13 @@ export default {
onSubmitModal() {
this.deleteBadge(this.badgeInModal)
.then(() => {
- createFlash({
+ createAlert({
message: s__('Badges|The badge was deleted.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('Badges|Deleting the badge failed, please try again.'),
});
throw error;
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index 2b0aaa74e83..feac6f10b1e 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -1,5 +1,5 @@
import { isEmpty } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { scrollToElement } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
@@ -18,7 +18,7 @@ export const addDraftToDiscussion = ({ commit }, { endpoint, data }) =>
return res;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred adding a draft to the thread.'),
});
});
@@ -32,7 +32,7 @@ export const createNewDraft = ({ commit }, { endpoint, data }) =>
return res;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred adding a new draft.'),
});
});
@@ -44,7 +44,7 @@ export const deleteDraft = ({ commit, getters }, draft) =>
commit(types.DELETE_DRAFT, draft.id);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while deleting the comment'),
}),
);
@@ -62,7 +62,7 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching pending comments'),
}),
);
@@ -122,7 +122,7 @@ export const updateDraft = (
.then((data) => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data))
.then(callback)
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while updating the comment'),
}),
);
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js
index df5214ea7ab..75e4ae63c18 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/getters.js
@@ -22,7 +22,11 @@ export const draftsPerFileHashAndLine = (state) =>
acc[draft.file_hash] = {};
}
- acc[draft.file_hash][draft.line_code] = draft;
+ if (!acc[draft.file_hash][draft.line_code]) {
+ acc[draft.file_hash][draft.line_code] = [];
+ }
+
+ acc[draft.file_hash][draft.line_code].push(draft);
}
return acc;
@@ -61,18 +65,15 @@ export const shouldRenderDraftRowInDiscussion = (state, getters) => (discussionI
export const draftForDiscussion = (state, getters) => (discussionId) =>
getters.draftsPerDiscussionId[discussionId] || {};
-export const draftForLine = (state, getters) => (diffFileSha, line, side = null) => {
+export const draftsForLine = (state, getters) => (diffFileSha, line, side = null) => {
const draftsForFile = getters.draftsPerFileHashAndLine[diffFileSha];
-
const key = side !== null ? parallelLineKey(line, side) : line.line_code;
+ const showDraftsForThisSide = showDraftOnSide(line, side);
- if (draftsForFile) {
- const draft = draftsForFile[key];
- if (draft && showDraftOnSide(line, side)) {
- return draft;
- }
+ if (showDraftsForThisSide && draftsForFile?.[key]) {
+ return draftsForFile[key];
}
- return {};
+ return [];
};
export const draftsForFile = (state) => (diffFileSha) =>
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 679940d1317..68f5180cc03 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -80,7 +80,7 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
success(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching Markdown preview'),
}),
);
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 23b66405844..3239375bf7c 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -145,6 +145,20 @@ export const LINK_TEXT = {
customizable: false,
};
+export const INDENT_LINE = {
+ id: 'editing.indentLine',
+ description: __('Indent line'),
+ defaultKeys: ['mod+]'], // eslint-disable-line @gitlab/require-i18n-strings
+ customizable: false,
+};
+
+export const OUTDENT_LINE = {
+ id: 'editing.outdentLine',
+ description: __('Outdent line'),
+ defaultKeys: ['mod+['], // eslint-disable-line @gitlab/require-i18n-strings
+ customizable: false,
+};
+
export const TOGGLE_MARKDOWN_PREVIEW = {
id: 'editing.toggleMarkdownPreview',
description: __('Toggle Markdown preview'),
diff --git a/app/assets/javascripts/blame/blame_redirect.js b/app/assets/javascripts/blame/blame_redirect.js
new file mode 100644
index 00000000000..155e2a3a2cd
--- /dev/null
+++ b/app/assets/javascripts/blame/blame_redirect.js
@@ -0,0 +1,23 @@
+import { setUrlParams } from '~/lib/utils/url_utility';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+
+export default function redirectToCorrectBlamePage() {
+ const { hash } = window.location;
+ const linesPerPage = parseInt(document.querySelector('.js-per-page').dataset.perPage, 10);
+ const params = new URLSearchParams(window.location.search);
+ const currentPage = parseInt(params.get('page'), 10);
+ const isPaginationDisabled = params.get('no_pagination');
+ if (hash && linesPerPage && !isPaginationDisabled) {
+ const lineNumber = parseInt(hash.split('#L')[1], 10);
+ const pageToRedirect = Math.ceil(lineNumber / linesPerPage);
+ const isRedirectNeeded =
+ (pageToRedirect > 1 && pageToRedirect !== currentPage) || pageToRedirect < currentPage;
+ if (isRedirectNeeded) {
+ createAlert({
+ message: __('Please wait a few moments while we load the file history for this line.'),
+ });
+ window.location.href = setUrlParams({ page: pageToRedirect });
+ }
+ }
+}
diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js
index 2831c37838b..4fbc9044cf0 100644
--- a/app/assets/javascripts/blob/3d_viewer/index.js
+++ b/app/assets/javascripts/blob/3d_viewer/index.js
@@ -1,6 +1,6 @@
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader';
-import * as THREE from 'three/build/three.module';
+import * as THREE from 'three';
import MeshObject from './mesh_object';
export default class Renderer {
diff --git a/app/assets/javascripts/blob/3d_viewer/mesh_object.js b/app/assets/javascripts/blob/3d_viewer/mesh_object.js
index 5322dc00e86..6c816b2d07f 100644
--- a/app/assets/javascripts/blob/3d_viewer/mesh_object.js
+++ b/app/assets/javascripts/blob/3d_viewer/mesh_object.js
@@ -1,4 +1,4 @@
-import { Matrix4, MeshLambertMaterial, Mesh } from 'three/build/three.module';
+import { Matrix4, MeshLambertMaterial, Mesh } from 'three';
const defaultColor = 0xe24329;
const materials = {
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index 0a5bcf326a1..df38c5400e2 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -22,6 +22,7 @@ const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
};
function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, elementsToUpdate) {
+ if (!blobContentHolder) return;
const updateBlameAndBlobPermalinkCb = () => {
// Wait for the hash to update from the LineHighlighter callback
setTimeout(() => {
diff --git a/app/assets/javascripts/blob/components/table_contents.vue b/app/assets/javascripts/blob/components/table_contents.vue
index 07da262ec9a..b3410b94b98 100644
--- a/app/assets/javascripts/blob/components/table_contents.vue
+++ b/app/assets/javascripts/blob/components/table_contents.vue
@@ -26,6 +26,8 @@ export default {
} else if (blobViewerAttr('data-loaded') === 'true') {
this.isHidden = false;
this.generateHeaders();
+
+ this.observer.disconnect();
}
});
@@ -47,13 +49,11 @@ export default {
if (headers.length) {
const firstHeader = getHeaderNumber(headers[0]);
- headers.forEach((el) => {
- this.items.push({
- text: el.textContent.trim(),
- anchor: el.querySelector('a').getAttribute('id'),
- spacing: Math.max((getHeaderNumber(el) - firstHeader) * 8, 0),
- });
- });
+ this.items = headers.map((el) => ({
+ text: el.textContent.trim(),
+ anchor: el.querySelector('a').getAttribute('id'),
+ spacing: Math.max((getHeaderNumber(el) - firstHeader) * 8, 0),
+ }));
}
},
},
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 991f98c89e7..adc2649e5df 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import Api from '~/api';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
@@ -155,7 +155,7 @@ export default class FileTemplateMediator {
}
})
.catch((err) =>
- createFlash({
+ createAlert({
message: __(`An error occurred while fetching the template: ${err}`),
}),
);
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 4c497db9842..44b75cc3e68 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -1,5 +1,5 @@
import { SwaggerUIBundle } from 'swagger-ui-dist';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
export default () => {
@@ -15,7 +15,7 @@ export default () => {
});
})
.catch((error) => {
- createFlash({
+ createAlert({
message: __('Something went wrong while initializing the OpenAPI viewer'),
});
throw error;
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 5ca3f131d99..8d323c335d3 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
REPO_BLOB_LOAD_VIEWER_START,
@@ -69,7 +69,7 @@ export const handleBlobRichViewer = (viewer, type) => {
loadRichBlobViewer(type)
.then((module) => module?.default(viewer))
.catch((error) => {
- createFlash({
+ createAlert({
message: __('Error loading file viewer.'),
});
throw error;
@@ -221,7 +221,7 @@ export class BlobViewer {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading viewer'),
}),
);
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index d73e1cc43b0..1c9c99dcc2f 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import BlobFileDropzone from '../blob/blob_file_dropzone';
@@ -79,7 +79,7 @@ export default () => {
initPopovers();
})
.catch((e) =>
- createFlash({
+ createAlert({
message: e,
}),
);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 2ee2e199358..78e3f934183 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -3,7 +3,7 @@ import { SourceEditorExtension } from '~/editor/extensions/source_editor_extensi
import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
import { ToolbarExtension } from '~/editor/extensions/source_editor_toolbar_ext';
import SourceEditor from '~/editor/source_editor';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
@@ -44,7 +44,7 @@ export default class EditBlob {
},
]);
} catch (e) {
- createFlash({
+ createAlert({
message: `${BLOB_EDITOR_ERROR}: ${e}`,
});
}
@@ -130,7 +130,7 @@ export default class EditBlob {
currentPane.renderGFM();
})
.catch(() =>
- createFlash({
+ createAlert({
message: BLOB_PREVIEW_ERROR,
}),
);
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 92a623d65d4..3a2b11a649d 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -17,9 +17,9 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/toolt
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { ListType } from '../constants';
import eventHub from '../eventhub';
-import BoardBlockedIcon from './board_blocked_icon.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
@@ -34,7 +34,7 @@ export default {
IssueDueDate,
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
- BoardBlockedIcon,
+ IssuableBlockedIcon,
GlSprintf,
BoardCardMoveToPosition,
WorkItemTypeIcon,
@@ -218,7 +218,7 @@ export default {
<div>
<div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0 gl-mr-3 gl-font-base gl-overflow-break-word">
- <board-blocked-icon
+ <issuable-blocked-icon
v-if="item.blocked"
:item="item"
:unique-id="`${item.id}${list.id}`"
@@ -250,7 +250,8 @@ export default {
>{{ item.title }}</a
>
</h4>
- <board-card-move-to-position :item="item" :list="list" :index="index" />
+ <!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
+ <board-card-move-to-position v-if="!isEpicBoard" :item="item" :list="list" :index="index" />
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
@@ -397,7 +398,6 @@ export default {
:img-size="avatarSize"
class="js-no-trigger user-avatar-link"
tooltip-placement="bottom"
- :enforce-gl-avatar="true"
>
<span class="js-assignee-tooltip">
<span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index fa0c798ca9d..11a5d89cc8c 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -8,6 +8,7 @@ import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
FILTER_ANY,
+ TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { AssigneeFilterType } from '~/boards/constants';
@@ -55,6 +56,7 @@ export default {
myReactionEmoji,
releaseTag,
confidential,
+ healthStatus,
} = this.filterParams;
const filteredSearchValue = [];
@@ -154,6 +156,13 @@ export default {
});
}
+ if (healthStatus) {
+ filteredSearchValue.push({
+ type: TOKEN_TYPE_HEALTH,
+ value: { data: healthStatus, operator: '=' },
+ });
+ }
+
if (this.filterParams['not[authorUsername]']) {
filteredSearchValue.push({
type: 'author',
@@ -248,6 +257,7 @@ export default {
iterationCadenceId,
releaseTag,
confidential,
+ healthStatus,
} = this.filterParams;
let iteration = iterationId;
let cadence = iterationCadenceId;
@@ -292,6 +302,7 @@ export default {
my_reaction_emoji: myReactionEmoji,
release_tag: releaseTag,
confidential,
+ [TOKEN_TYPE_HEALTH]: healthStatus,
},
(value) => {
if (value || value === false) {
@@ -390,6 +401,9 @@ export default {
case 'filtered-search-term':
if (filter.value.data) plainText.push(filter.value.data);
break;
+ case TOKEN_TYPE_HEALTH:
+ filterParams.healthStatus = filter.value.data;
+ break;
default:
break;
}
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 9f359a25234..eb889344c1e 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -4,7 +4,6 @@ import { mapGetters, mapActions, mapState } from 'vuex';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { formType } from '../constants';
import createBoardMutation from '../graphql/board_create.mutation.graphql';
@@ -45,7 +44,6 @@ export default {
BoardConfigurationOptions,
GlAlert,
},
- mixins: [glFeatureFlagMixin()],
inject: {
fullPath: {
default: '',
@@ -233,9 +231,8 @@ export default {
}
},
setIteration(iteration) {
- if (this.glFeatures.iterationCadences) {
- this.board.iterationCadenceId = iteration.iterationCadenceId;
- }
+ this.board.iterationCadenceId = iteration.iterationCadenceId;
+
this.$set(this.board, 'iteration', {
id: iteration.id,
});
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index ed22a375271..91b7f5004ad 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -2,8 +2,6 @@ import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql
import { __ } from '~/locale';
import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
-import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
-import boardBlockingEpicsQuery from './graphql/board_blocking_epics.query.graphql';
import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql';
import updateBoardListMutation from './graphql/board_list_update.mutation.graphql';
@@ -67,15 +65,6 @@ export const listsQuery = {
},
};
-export const blockingIssuablesQueries = {
- [issuableTypes.issue]: {
- query: boardBlockingIssuesQuery,
- },
- [issuableTypes.epic]: {
- query: boardBlockingEpicsQuery,
- },
-};
-
export const updateListQueries = {
[issuableTypes.issue]: {
mutation: updateBoardListMutation,
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 854717ed4c4..a7003edba47 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -86,6 +86,7 @@ function mountBoardApp(el) {
milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable),
assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable),
iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
+ healthStatusFeatureAvailable: parseBoolean(el.dataset.healthStatusFeatureAvailable),
allowScopedLabels: parseBoolean(el.dataset.scopedLabels),
swimlanesFeatureAvailable: gon.licensed_features?.swimlanes,
multipleIssueBoardsAvailable: parseBoolean(el.dataset.multipleBoardsAvailable),
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index 17fd3939441..d05b53f1a50 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import DivergenceGraph from './components/divergence_graph.vue';
@@ -51,7 +51,7 @@ export default (endpoint, defaultBranch) => {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching diverging counts for branches. Please try again.'),
}),
);
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index fea4b56153f..1f8096da94d 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -1,5 +1,13 @@
<script>
-import { GlTable, GlButton, GlBadge, GlTooltipDirective, GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ GlButton,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -15,14 +23,15 @@ export default {
),
},
components: {
- GlTable,
- GlButton,
- GlBadge,
ClipboardButton,
- TooltipOnTruncate,
- GlAvatarLink,
+ GlAlert,
GlAvatar,
+ GlAvatarLink,
+ GlBadge,
+ GlButton,
+ GlTable,
TimeAgoTooltip,
+ TooltipOnTruncate,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -138,13 +147,15 @@ export default {
/>
</template>
</gl-table>
- <div
+ <gl-alert
v-else
+ variant="warning"
+ :dismissible="false"
+ :show-icon="false"
data-testid="no_triggers_content"
data-qa-selector="no_triggers_content"
- class="settings-message gl-text-center gl-mb-3"
>
{{ s__('Pipelines|No triggers have been created yet. Add one using the form above.') }}
- </div>
+ </gl-alert>
</div>
</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
index 59ddf4b19d8..8d891ff1746 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
@@ -1,5 +1,7 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import { reportMessageToSentry } from '../utils';
import getAdminVariables from '../graphql/queries/variables.query.graphql';
import {
ADD_MUTATION_ACTION,
@@ -21,7 +23,11 @@ export default {
data() {
return {
adminVariables: [],
+ hasNextPage: false,
isInitialLoading: true,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
};
},
apollo: {
@@ -30,8 +36,29 @@ export default {
update(data) {
return data?.ciVariables?.nodes || [];
},
+ result({ data }) {
+ this.pageInfo = data?.ciVariables?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
error() {
- createFlash({ message: variableFetchErrorText });
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
},
watchLoading(flag) {
if (!flag) {
@@ -42,7 +69,10 @@ export default {
},
computed: {
isLoading() {
- return this.$apollo.queries.adminVariables.loading && this.isInitialLoading;
+ return (
+ (this.$apollo.queries.adminVariables.loading && this.isInitialLoading) ||
+ this.isLoadingMoreItems
+ );
},
},
methods: {
@@ -52,6 +82,15 @@ export default {
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.adminVariables.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
@@ -66,10 +105,9 @@ export default {
},
});
- const { errors } = data[currentMutation.name];
-
- if (errors.length > 0) {
- createFlash({ message: errors[0] });
+ if (data[currentMutation.name]?.errors?.length) {
+ const { errors } = data[currentMutation.name];
+ createAlert({ message: errors[0] });
} else {
// The writing to cache for admin variable is not working
// because there is no ID in the cache at the top level.
@@ -77,10 +115,14 @@ export default {
this.$apollo.queries.adminVariables.refetch();
}
} catch {
- createFlash({ message: genericMutationErrorText });
+ createAlert({ message: genericMutationErrorText });
}
},
},
+ componentName: 'InstanceVariables',
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
mutationData: {
[ADD_MUTATION_ACTION]: { action: addAdminVariable, name: 'addAdminVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateAdminVariable, name: 'updateAdminVariable' },
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
index 3522243e3e7..4af696b8dab 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -1,7 +1,9 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { reportMessageToSentry } from '../utils';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
@@ -25,6 +27,10 @@ export default {
data() {
return {
groupVariables: [],
+ hasNextPage: false,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
};
},
apollo: {
@@ -38,8 +44,28 @@ export default {
update(data) {
return data?.group?.ciVariables?.nodes || [];
},
+ result({ data }) {
+ this.pageInfo = data?.group?.ciVariables?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
error() {
- createFlash({ message: variableFetchErrorText });
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
},
},
},
@@ -48,7 +74,7 @@ export default {
return this.glFeatures.groupScopedCiVariables;
},
isLoading() {
- return this.$apollo.queries.groupVariables.loading;
+ return this.$apollo.queries.groupVariables.loading || this.isLoadingMoreItems;
},
},
methods: {
@@ -58,6 +84,16 @@ export default {
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.groupVariables.fetchMore({
+ variables: {
+ fullPath: this.groupPath,
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
@@ -74,16 +110,19 @@ export default {
},
});
- const { errors } = data[currentMutation.name];
-
- if (errors.length > 0) {
- createFlash({ message: errors[0] });
+ if (data[currentMutation.name]?.errors?.length) {
+ const { errors } = data[currentMutation.name];
+ createAlert({ message: errors[0] });
}
} catch {
- createFlash({ message: genericMutationErrorText });
+ createAlert({ message: genericMutationErrorText });
}
},
},
+ componentName: 'GroupVariables',
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
mutationData: {
[ADD_MUTATION_ACTION]: { action: addGroupVariable, name: 'addGroupVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateGroupVariable, name: 'updateGroupVariable' },
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
index 29db02a3c59..6bd549817f8 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
@@ -1,9 +1,10 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
-import { mapEnvironmentNames } from '../utils';
+import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
@@ -25,6 +26,10 @@ export default {
inject: ['endpoint', 'projectFullPath', 'projectId'],
data() {
return {
+ hasNextPage: false,
+ isLoadingMoreItems: false,
+ loadingCounter: 0,
+ pageInfo: {},
projectEnvironments: [],
projectVariables: [],
};
@@ -41,21 +46,42 @@ export default {
return mapEnvironmentNames(data?.project?.environments?.nodes);
},
error() {
- createFlash({ message: environmentFetchErrorText });
+ createAlert({ message: environmentFetchErrorText });
},
},
projectVariables: {
query: getProjectVariables,
variables() {
return {
+ after: null,
fullPath: this.projectFullPath,
};
},
update(data) {
return data?.project?.ciVariables?.nodes || [];
},
+ result({ data }) {
+ this.pageInfo = data?.project?.ciVariables?.pageInfo || this.pageInfo;
+ this.hasNextPage = this.pageInfo?.hasNextPage || false;
+ // Because graphQL has a limit of 100 items,
+ // we batch load all the variables by making successive queries
+ // to keep the same UX. As a safeguard, we make sure that we cannot go over
+ // 20 consecutive API calls, which means 2000 variables loaded maximum.
+ if (!this.hasNextPage) {
+ this.isLoadingMoreItems = false;
+ } else if (this.loadingCounter < 20) {
+ this.hasNextPage = false;
+ this.fetchMoreVariables();
+ this.loadingCounter += 1;
+ } else {
+ createAlert({ message: this.$options.tooManyCallsError });
+ reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
+ }
+ },
error() {
- createFlash({ message: variableFetchErrorText });
+ this.isLoadingMoreItems = false;
+ this.hasNextPage = false;
+ createAlert({ message: variableFetchErrorText });
},
},
},
@@ -63,7 +89,8 @@ export default {
isLoading() {
return (
this.$apollo.queries.projectVariables.loading ||
- this.$apollo.queries.projectEnvironments.loading
+ this.$apollo.queries.projectEnvironments.loading ||
+ this.isLoadingMoreItems
);
},
},
@@ -74,6 +101,16 @@ export default {
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
+ fetchMoreVariables() {
+ this.isLoadingMoreItems = true;
+
+ this.$apollo.queries.projectVariables.fetchMore({
+ variables: {
+ fullPath: this.projectFullPath,
+ after: this.pageInfo.endCursor,
+ },
+ });
+ },
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
@@ -89,16 +126,19 @@ export default {
variable,
},
});
-
- const { errors } = data[currentMutation.name];
- if (errors.length > 0) {
- createFlash({ message: errors[0] });
+ if (data[currentMutation.name]?.errors?.length) {
+ const { errors } = data[currentMutation.name];
+ createAlert({ message: errors[0] });
}
- } catch (e) {
- createFlash({ message: genericMutationErrorText });
+ } catch {
+ createAlert({ message: genericMutationErrorText });
}
},
},
+ componentName: 'ProjectVariables',
+ i18n: {
+ tooManyCallsError: __('Maximum number of variables loaded (2000)'),
+ },
mutationData: {
[ADD_MUTATION_ACTION]: { action: addProjectVariable, name: 'addProjectVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateProjectVariable, name: 'updateProjectVariable' },
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 1bb94080694..959ef6864fb 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -75,8 +75,7 @@ export default {
props: {
isLoading: {
type: Boolean,
- required: false,
- default: false,
+ required: true,
},
variables: {
type: Array,
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
index 9acc9fbffb6..f1fe188348d 100644
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
@@ -9,10 +9,10 @@ export default {
LegacyCiVariableTable,
},
computed: {
- ...mapState(['isGroup']),
+ ...mapState(['isGroup', 'isProject']),
},
mounted() {
- if (!this.isGroup) {
+ if (this.isProject) {
this.fetchEnvironments();
}
},
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index e2dd28cdaa1..ccad08ef8b6 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -52,7 +52,7 @@ export const groupString = 'Group';
// eslint-disable-next-line @gitlab/require-i18n-strings
export const instanceString = 'Instance';
// eslint-disable-next-line @gitlab/require-i18n-strings
-export const projectString = 'Instance';
+export const projectString = 'Project';
export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
export const AWS_TIP_MESSAGE = __(
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
index c6dd6d4faaf..b5555fe4401 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -1,9 +1,13 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-query getGroupVariables($fullPath: ID!) {
+query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
group(fullPath: $fullPath) {
id
- ciVariables {
+ ciVariables(after: $after, first: $first) {
+ pageInfo {
+ ...PageInfo
+ }
nodes {
...BaseCiVariable
... on CiGroupVariable {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
index a60a50e4bc4..08b5bf7af16 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
@@ -1,9 +1,13 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-query getProjectVariables($fullPath: ID!) {
+query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
project(fullPath: $fullPath) {
id
- ciVariables {
+ ciVariables(after: $after, first: $first) {
+ pageInfo {
+ ...PageInfo
+ }
nodes {
...BaseCiVariable
environmentScope
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
index 95056842b49..2667d6606fe 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
@@ -1,7 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-query getVariables {
- ciVariables {
+query getVariables($after: String, $first: Int = 100) {
+ ciVariables(after: $after, first: $first) {
+ pageInfo {
+ ...PageInfo
+ }
nodes {
...BaseCiVariable
... on CiInstanceVariable {
diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/settings.js
index c041531ae30..ecdc4f220bd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
+++ b/app/assets/javascripts/ci_variable_list/graphql/settings.js
@@ -3,7 +3,7 @@ import {
convertObjectPropsToCamelCase,
convertObjectPropsToSnakeCase,
} from '../../lib/utils/common_utils';
-import { getIdFromGraphQLId } from '../../graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '../../graphql_shared/utils';
import {
GRAPHQL_GROUP_TYPE,
GRAPHQL_PROJECT_TYPE,
@@ -30,6 +30,7 @@ const mapVariableTypes = (variables = [], kind) => {
return {
__typename: `Ci${kind}Variable`,
...convertObjectPropsToCamelCase(ciVar),
+ id: convertToGraphQLId('Ci::Variable', ciVar.id),
variableType: ciVar.variable_type ? ciVar.variable_type.toUpperCase() : ciVar.variableType,
};
});
@@ -40,9 +41,16 @@ const prepareProjectGraphQLResponse = ({ data, projectId, errors = [] }) => {
errors,
project: {
__typename: GRAPHQL_PROJECT_TYPE,
- id: projectId,
+ id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, projectId),
ciVariables: {
- __typename: 'CiVariableConnection',
+ __typename: `Ci${GRAPHQL_PROJECT_TYPE}VariableConnection`,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
nodes: mapVariableTypes(data.variables, projectString),
},
},
@@ -54,9 +62,16 @@ const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => {
errors,
group: {
__typename: GRAPHQL_GROUP_TYPE,
- id: groupId,
+ id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, groupId),
ciVariables: {
- __typename: 'CiVariableConnection',
+ __typename: `Ci${GRAPHQL_GROUP_TYPE}VariableConnection`,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
nodes: mapVariableTypes(data.variables, groupString),
},
},
@@ -68,24 +83,42 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
errors,
ciVariables: {
__typename: `Ci${instanceString}VariableConnection`,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
nodes: mapVariableTypes(data.variables, instanceString),
},
};
};
-const callProjectEndpoint = async ({
+async function callProjectEndpoint({
endpoint,
fullPath,
variable,
projectId,
cache,
destroy = false,
-}) => {
+}) {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- return prepareProjectGraphQLResponse({ data, projectId });
+
+ const graphqlData = prepareProjectGraphQLResponse({ data, projectId });
+
+ cache.writeQuery({
+ query: getProjectVariables,
+ variables: {
+ fullPath,
+ after: null,
+ },
+ data: graphqlData,
+ });
+ return graphqlData;
} catch (e) {
return prepareProjectGraphQLResponse({
data: cache.readQuery({ query: getProjectVariables, variables: { fullPath } }),
@@ -93,7 +126,7 @@ const callProjectEndpoint = async ({
errors: [...e.response.data],
});
}
-};
+}
const callGroupEndpoint = async ({
endpoint,
@@ -107,7 +140,15 @@ const callGroupEndpoint = async ({
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- return prepareGroupGraphQLResponse({ data, groupId });
+
+ const graphqlData = prepareGroupGraphQLResponse({ data, groupId });
+
+ cache.writeQuery({
+ query: getGroupVariables,
+ data: graphqlData,
+ });
+
+ return graphqlData;
} catch (e) {
return prepareGroupGraphQLResponse({
data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }),
@@ -123,7 +164,14 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false })
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
- return prepareAdminGraphQLResponse({ data });
+ const graphqlData = prepareAdminGraphQLResponse({ data });
+
+ cache.writeQuery({
+ query: getAdminVariables,
+ data: graphqlData,
+ });
+
+ return graphqlData;
} catch (e) {
return prepareAdminGraphQLResponse({
data: cache.readQuery({ query: getAdminVariables }),
@@ -163,3 +211,46 @@ export const resolvers = {
},
},
};
+
+export const mergeVariables = (existing, incoming, { args }) => {
+ if (!existing || !args?.after) {
+ return incoming;
+ }
+
+ const { nodes, ...rest } = incoming;
+ const result = rest;
+ result.nodes = [...existing.nodes, ...nodes];
+
+ return result;
+};
+
+export const cacheConfig = {
+ cacheConfig: {
+ typePolicies: {
+ Query: {
+ fields: {
+ ciVariables: {
+ keyArgs: false,
+ merge: mergeVariables,
+ },
+ },
+ },
+ Project: {
+ fields: {
+ ciVariables: {
+ keyArgs: ['fullPath', 'endpoint', 'projectId'],
+ merge: mergeVariables,
+ },
+ },
+ },
+ Group: {
+ fields: {
+ ciVariables: {
+ keyArgs: ['fullPath'],
+ merge: mergeVariables,
+ },
+ },
+ },
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index f5bdd4c7b1e..1b69da9e086 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -6,7 +6,7 @@ import CiAdminVariables from './components/ci_admin_variables.vue';
import CiGroupVariables from './components/ci_group_variables.vue';
import CiProjectVariables from './components/ci_project_variables.vue';
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
-import { resolvers } from './graphql/resolvers';
+import { cacheConfig, resolvers } from './graphql/settings';
import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
@@ -45,7 +45,7 @@ const mountCiVariableListApp = (containerEl) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(resolvers, cacheConfig),
});
return new Vue({
@@ -81,6 +81,7 @@ const mountLegacyCiVariableListApp = (containerEl) => {
endpoint,
projectId,
isGroup,
+ isProject,
maskableRegex,
protectedByDefault,
awsLogoSvgPath,
@@ -92,6 +93,8 @@ const mountLegacyCiVariableListApp = (containerEl) => {
maskedEnvironmentVariablesLink,
environmentScopeLink,
} = containerEl.dataset;
+
+ const parsedIsProject = parseBoolean(isProject);
const parsedIsGroup = parseBoolean(isGroup);
const isProtectedByDefault = parseBoolean(protectedByDefault);
@@ -99,6 +102,7 @@ const mountLegacyCiVariableListApp = (containerEl) => {
endpoint,
projectId,
isGroup: parsedIsGroup,
+ isProject: parsedIsProject,
maskableRegex,
isProtectedByDefault,
awsLogoSvgPath,
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
index 8a182737e7b..ac31e845b0d 100644
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ b/app/assets/javascripts/ci_variable_list/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -48,7 +48,7 @@ export const addVariable = ({ state, dispatch }) => {
dispatch('fetchVariables');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error.response.data[0],
});
dispatch('receiveAddVariableError', error);
@@ -80,7 +80,7 @@ export const updateVariable = ({ state, dispatch }) => {
dispatch('fetchVariables');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error.response.data[0],
});
dispatch('receiveUpdateVariableError', error);
@@ -109,7 +109,7 @@ export const fetchVariables = ({ dispatch, state }) => {
dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables));
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('There was an error fetching the variables.'),
});
});
@@ -139,7 +139,7 @@ export const deleteVariable = ({ dispatch, state }) => {
dispatch('fetchVariables');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error.response.data[0],
});
dispatch('receiveDeleteVariableError', error);
@@ -162,7 +162,7 @@ export const fetchEnvironments = ({ dispatch, state }) => {
dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data));
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('There was an error fetching the environments information.'),
});
});
diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci_variable_list/utils.js
index 1faa97a5f73..eeca69274ce 100644
--- a/app/assets/javascripts/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci_variable_list/utils.js
@@ -1,3 +1,4 @@
+import * as Sentry from '@sentry/browser';
import { uniq } from 'lodash';
import { allEnvironments } from './constants';
@@ -48,3 +49,12 @@ export const convertEnvironmentScope = (environmentScope = '') => {
export const mapEnvironmentNames = (nodes = []) => {
return nodes.map((env) => env.name);
};
+
+export const reportMessageToSentry = (component, message, context) => {
+ Sentry.withScope((scope) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ scope.setContext('Vue data', context);
+ scope.setTag('component', component);
+ Sentry.captureMessage(message);
+ });
+};
diff --git a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
index 18c6503bfb2..ca65665b9ed 100644
--- a/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
+++ b/app/assets/javascripts/clusters/agents/components/activity_events_list.vue
@@ -132,11 +132,7 @@ export default {
:key="key"
class="agent-activity-list issuable-discussion"
>
- <h4
- class="gl-pb-4 gl-ml-5"
- :class="$options.borderClasses"
- data-testid="activity-section-title"
- >
+ <h4 class="gl-pb-4" :class="$options.borderClasses" data-testid="activity-section-title">
{{ key }}
</h4>
diff --git a/app/assets/javascripts/clusters/agents/components/activity_history_item.vue b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
index 7792d89a575..ed11fe1130d 100644
--- a/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
+++ b/app/assets/javascripts/clusters/agents/components/activity_history_item.vue
@@ -45,8 +45,8 @@ export default {
};
</script>
<template>
- <history-item :icon="eventDetails.eventTypeIcon" class="gl-my-0! gl-pr-0!">
- <strong>
+ <history-item :icon="eventDetails.eventTypeIcon" class="gl-my-0! gl-px-0! gl-pb-0!">
+ <strong class="gl-pl-3 gl-font-lg">
<gl-sprintf :message="eventDetails.title"
><template v-if="eventDetails.titleIcon" #titleIcon
><gl-icon
@@ -61,15 +61,15 @@ export default {
</strong>
<template #body>
- <p class="gl-mt-2 gl-mb-0 gl-pb-2" :class="bodyClass">
+ <p class="gl-mt-2 gl-mb-0 gl-ml-3 gl-pb-3 gl-text-secondary" :class="bodyClass">
<gl-sprintf :message="eventDetails.body">
<template #userName>
- <span class="gl-font-weight-bold">{{ eventDetails.user.name }}</span>
+ <span class="gl-font-weight-bold gl-text-body">{{ eventDetails.user.name }}</span>
<gl-link :href="eventDetails.user.webUrl">@{{ eventDetails.user.username }}</gl-link>
</template>
<template #strong="{ content }">
- <span class="gl-font-weight-bold"> {{ content }} </span>
+ <span class="gl-font-weight-bold gl-text-body"> {{ content }} </span>
</template>
</gl-sprintf>
<time-ago-tooltip :time="eventDetails.recordedAt" />
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index a8fef372637..21524c5b29e 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,7 +1,7 @@
import { GlToast } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AccessorUtilities from '~/lib/utils/accessor';
import initProjectSelectDropdown from '~/project_select';
import Poll from '~/lib/utils/poll';
@@ -196,7 +196,7 @@ export default class Clusters {
}
static handleError() {
- createFlash({
+ createAlert({
message: s__('ClusterIntegration|Something went wrong on our end.'),
});
}
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index d70b36e63bc..77d6d5eb009 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
@@ -70,7 +70,7 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
- createFlash({
+ createAlert({
message: s__('Clusters|An error occurred while loading clusters'),
});
diff --git a/app/assets/javascripts/code_navigation/utils/dom_utils.js b/app/assets/javascripts/code_navigation/utils/dom_utils.js
index 90af31b715c..66770cca8a2 100644
--- a/app/assets/javascripts/code_navigation/utils/dom_utils.js
+++ b/app/assets/javascripts/code_navigation/utils/dom_utils.js
@@ -6,26 +6,28 @@ const isBlank = (str) => !str || /^\s*$/.test(str);
const isMatch = (s1, s2) => !isBlank(s1) && s1.trim() === s2.trim();
-const createSpan = (content) => {
+const createSpan = (content, classList) => {
const span = document.createElement('span');
span.innerText = content;
+ span.classList = classList || '';
return span;
};
-const wrapSpacesWithSpans = (text) => text.replace(/ /g, createSpan(' ').outerHTML);
+const wrapSpacesWithSpans = (text) =>
+ text.replace(/ /g, createSpan(' ').outerHTML).replace(/\t/g, createSpan(' ').outerHTML);
-const wrapTextWithSpan = (el, text) => {
+const wrapTextWithSpan = (el, text, classList) => {
if (isTextNode(el) && isMatch(el.textContent, text)) {
- const newEl = createSpan(text.trim());
+ const newEl = createSpan(text.trim(), classList);
el.replaceWith(newEl);
}
};
-const wrapNodes = (text) => {
+const wrapNodes = (text, classList) => {
const wrapper = createSpan();
// eslint-disable-next-line no-unsanitized/property
wrapper.innerHTML = wrapSpacesWithSpans(text);
- wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text));
+ wrapper.childNodes.forEach((el) => wrapTextWithSpan(el, text, classList));
return wrapper.childNodes;
};
diff --git a/app/assets/javascripts/code_navigation/utils/index.js b/app/assets/javascripts/code_navigation/utils/index.js
index 46038df2f86..7a5fa9f4a35 100644
--- a/app/assets/javascripts/code_navigation/utils/index.js
+++ b/app/assets/javascripts/code_navigation/utils/index.js
@@ -17,11 +17,9 @@ export const addInteractionClass = ({ path, d, wrapTextNodes }) => {
if (wrapTextNodes) {
line.childNodes.forEach((elm) => {
- if (isTextNode(elm)) {
- // Highlight.js does not wrap all text nodes by default
- // We need all text nodes to be wrapped in order to append code nav attributes
- elm.replaceWith(...wrapNodes(elm.textContent));
- }
+ // Highlight.js does not wrap all text nodes by default
+ // We need all text nodes to be wrapped in order to append code nav attributes
+ elm.replaceWith(...wrapNodes(elm.textContent, elm.classList));
});
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 6890d7f6f44..b0a1c46e619 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -205,6 +205,12 @@ export default {
mrPipelinesDocsPath: helpPagePath('ci/pipelines/merge_request_pipelines.md', {
anchor: 'prerequisites',
}),
+ runPipelinesInTheParentProjectHelpPath: helpPagePath(
+ '/ci/pipelines/merge_request_pipelines.html',
+ {
+ anchor: 'run-pipelines-in-the-parent-project',
+ },
+ ),
};
</script>
<template>
@@ -321,10 +327,7 @@ export default {
s__('Pipelines|If you are unsure, please ask a project maintainer to review it for you.')
}}
</p>
- <gl-link
- href="/help/ci/pipelines/merge_request_pipelines.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
- target="_blank"
- >
+ <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
{{ s__('Pipelines|More Information') }}
</gl-link>
</gl-modal>
diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js
index f973bf51b57..d40cbe589c0 100644
--- a/app/assets/javascripts/commit_merge_requests.js
+++ b/app/assets/javascripts/commit_merge_requests.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
import { n__, s__ } from './locale';
@@ -71,7 +71,7 @@ export function fetchCommitMergeRequests() {
$container.html($content);
})
.catch(() =>
- createFlash({
+ createAlert({
message: s__('Commits|An error occurred while fetching merge requests data.'),
}),
);
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index af049738016..e95424eef4d 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Api from '~/api';
import { __ } from '~/locale';
import state from '../state';
@@ -80,7 +80,7 @@ export default {
this.selectProject(this.projects[0]);
})
.catch((e) => {
- createFlash({
+ createAlert({
message: __('Error fetching forked projects. Please try again.'),
});
throw e;
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 659c447e861..22381377389 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -3,7 +3,7 @@ import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { VARIANT_DANGER } from '~/flash';
import { createContentEditor } from '../services/create_content_editor';
-import { ALERT_EVENT } from '../constants';
+import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
@@ -51,6 +51,12 @@ export default {
required: false,
default: '',
},
+ autofocus: {
+ type: [String, Boolean],
+ required: false,
+ default: false,
+ validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus),
+ },
},
data() {
return {
@@ -67,7 +73,7 @@ export default {
},
},
created() {
- const { renderMarkdown, uploadsPath, extensions, serializerConfig } = this;
+ const { renderMarkdown, uploadsPath, extensions, serializerConfig, autofocus } = this;
// This is a non-reactive attribute intentionally since this is a complex object.
this.contentEditor = createContentEditor({
@@ -75,6 +81,9 @@ export default {
uploadsPath,
extensions,
serializerConfig,
+ tiptapOptions: {
+ autofocus,
+ },
});
},
mounted() {
@@ -141,7 +150,12 @@ export default {
<template>
<content-editor-provider :content-editor="contentEditor">
<div>
- <editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
+ <editor-state-observer
+ @docUpdate="notifyChange"
+ @focus="focus"
+ @blur="blur"
+ @keydown="$emit('keydown', $event)"
+ />
<content-editor-alert />
<div
data-testid="content-editor"
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
index 41c3771bf41..ccb46e3b593 100644
--- a/app/assets/javascripts/content_editor/components/editor_state_observer.vue
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -1,6 +1,6 @@
<script>
import { debounce } from 'lodash';
-import { ALERT_EVENT } from '../constants';
+import { ALERT_EVENT, KEYDOWN_EVENT } from '../constants';
export const tiptapToComponentMap = {
update: 'docUpdate',
@@ -10,7 +10,7 @@ export const tiptapToComponentMap = {
blur: 'blur',
};
-export const eventHubEvents = [ALERT_EVENT];
+export const eventHubEvents = [ALERT_EVENT, KEYDOWN_EVENT];
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
new file mode 100644
index 00000000000..987b7044272
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -0,0 +1,264 @@
+<script>
+import { GlDropdownItem, GlAvatarLabeled } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdownItem,
+ GlAvatarLabeled,
+ },
+
+ props: {
+ char: {
+ type: String,
+ required: true,
+ },
+
+ nodeType: {
+ type: String,
+ required: true,
+ },
+
+ nodeProps: {
+ type: Object,
+ required: true,
+ },
+
+ items: {
+ type: Array,
+ required: true,
+ },
+
+ command: {
+ type: Function,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ selectedIndex: 0,
+ };
+ },
+
+ computed: {
+ isReference() {
+ return this.nodeType.startsWith('reference');
+ },
+
+ isCommand() {
+ return this.isReference && this.nodeProps.referenceType === 'command';
+ },
+
+ isUser() {
+ return this.isReference && this.nodeProps.referenceType === 'user';
+ },
+
+ isIssue() {
+ return this.isReference && this.nodeProps.referenceType === 'issue';
+ },
+
+ isLabel() {
+ return this.isReference && this.nodeProps.referenceType === 'label';
+ },
+
+ isEpic() {
+ return this.isReference && this.nodeProps.referenceType === 'epic';
+ },
+
+ isSnippet() {
+ return this.isReference && this.nodeProps.referenceType === 'snippet';
+ },
+
+ isVulnerability() {
+ return this.isReference && this.nodeProps.referenceType === 'vulnerability';
+ },
+
+ isMergeRequest() {
+ return this.isReference && this.nodeProps.referenceType === 'merge_request';
+ },
+
+ isMilestone() {
+ return this.isReference && this.nodeProps.referenceType === 'milestone';
+ },
+
+ isEmoji() {
+ return this.nodeType === 'emoji';
+ },
+ },
+
+ watch: {
+ items() {
+ this.selectedIndex = 0;
+ },
+ },
+
+ methods: {
+ getText(item) {
+ if (this.isEmoji) return item.e;
+
+ switch (this.isReference && this.nodeProps.referenceType) {
+ case 'user':
+ return `${this.char}${item.username}`;
+ case 'issue':
+ case 'merge_request':
+ return `${this.char}${item.iid}`;
+ case 'snippet':
+ return `${this.char}${item.id}`;
+ case 'milestone':
+ return `${this.char}${item.title}`;
+ case 'label':
+ return item.title;
+ case 'command':
+ return `${this.char}${item.name}`;
+ case 'epic':
+ return item.reference;
+ case 'vulnerability':
+ return `[vulnerability:${item.id}]`;
+ default:
+ return '';
+ }
+ },
+
+ getProps(item) {
+ const props = {};
+
+ if (this.isEmoji) {
+ Object.assign(props, {
+ name: item.name,
+ unicodeVersion: item.u,
+ title: item.d,
+ moji: item.e,
+ });
+ }
+
+ if (this.isLabel || this.isMilestone) {
+ Object.assign(props, {
+ originalText: `${this.char}${
+ /\W/.test(item.title) ? JSON.stringify(item.title) : item.title
+ }`,
+ });
+ }
+
+ if (this.isLabel) {
+ Object.assign(props, {
+ text: item.title,
+ color: item.color,
+ });
+ }
+
+ Object.assign(props, this.nodeProps);
+
+ return props;
+ },
+
+ onKeyDown({ event }) {
+ if (event.key === 'ArrowUp') {
+ this.upHandler();
+ return true;
+ }
+
+ if (event.key === 'ArrowDown') {
+ this.downHandler();
+ return true;
+ }
+
+ if (event.key === 'Enter') {
+ this.enterHandler();
+ return true;
+ }
+
+ return false;
+ },
+
+ upHandler() {
+ this.selectedIndex = (this.selectedIndex + this.items.length - 1) % this.items.length;
+ },
+
+ downHandler() {
+ this.selectedIndex = (this.selectedIndex + 1) % this.items.length;
+ },
+
+ enterHandler() {
+ this.selectItem(this.selectedIndex);
+ },
+
+ selectItem(index) {
+ const item = this.items[index];
+
+ if (item) {
+ this.command({
+ text: this.getText(item),
+ ...this.getProps(item),
+ });
+ }
+ },
+
+ avatarSubLabel(item) {
+ return item.count ? `${item.name} (${item.count})` : item.name;
+ },
+ },
+};
+</script>
+
+<template>
+ <ul
+ :class="{ show: items.length > 0 }"
+ class="gl-new-dropdown dropdown-menu gl-relative"
+ data-testid="content-editor-suggestions-dropdown"
+ >
+ <div class="gl-new-dropdown-inner gl-overflow-y-auto">
+ <gl-dropdown-item
+ v-for="(item, index) in items"
+ :key="index"
+ :class="{ 'gl-bg-gray-50': index === selectedIndex }"
+ @click="selectItem(index)"
+ >
+ <gl-avatar-labeled
+ v-if="isUser"
+ :label="item.username"
+ :sub-label="avatarSubLabel(item)"
+ :src="item.avatar_url"
+ :entity-name="item.username"
+ :shape="item.type === 'Group' ? 'rect' : 'circle'"
+ :size="32"
+ />
+ <span v-if="isIssue || isMergeRequest">
+ <small>{{ item.iid }}</small>
+ {{ item.title }}
+ </span>
+ <span v-if="isVulnerability || isSnippet">
+ <small>{{ item.id }}</small>
+ {{ item.title }}
+ </span>
+ <span v-if="isEpic">
+ <small>{{ item.reference }}</small>
+ {{ item.title }}
+ </span>
+ <span v-if="isMilestone">
+ {{ item.title }}
+ </span>
+ <span v-if="isLabel" class="gl-display-flex gl-align-items-center">
+ <span
+ data-testid="label-color-box"
+ class="gl-rounded-base gl-display-block gl-w-5 gl-h-5 gl-mr-3"
+ :style="{ backgroundColor: item.color }"
+ ></span>
+ {{ item.title }}
+ </span>
+ <span v-if="isCommand">
+ /{{ item.name }} <small> {{ item.params[0] }} </small><br />
+ <em>
+ <small> {{ item.description }} </small>
+ </em>
+ </span>
+ <div v-if="isEmoji" class="gl-display-flex gl-align-items-center">
+ <div class="gl-pr-4 gl-font-lg">{{ item.e }}</div>
+ <div class="gl-flex-grow-1">
+ {{ item.name }}<br />
+ <small>{{ item.d }}</small>
+ </div>
+ </div>
+ </gl-dropdown-item>
+ </div>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/label.vue b/app/assets/javascripts/content_editor/components/wrappers/label.vue
new file mode 100644
index 00000000000..4206c866032
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/label.vue
@@ -0,0 +1,34 @@
+<script>
+import { NodeViewWrapper } from '@tiptap/vue-2';
+import { GlLabel } from '@gitlab/ui';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ name: 'DetailsWrapper',
+ components: {
+ NodeViewWrapper,
+ GlLabel,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ isScopedLabel() {
+ return isScopedLabel({ title: this.node.attrs.originalText });
+ },
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-display-inline-block">
+ <gl-label
+ size="sm"
+ :scoped="isScopedLabel"
+ :background-color="node.attrs.color"
+ :title="node.attrs.text"
+ />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/constants/index.js b/app/assets/javascripts/content_editor/constants/index.js
index 564cca23afa..14862727811 100644
--- a/app/assets/javascripts/content_editor/constants/index.js
+++ b/app/assets/javascripts/content_editor/constants/index.js
@@ -42,10 +42,8 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
},
];
-export const LOADING_CONTENT_EVENT = 'loading';
-export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
-export const LOADING_ERROR_EVENT = 'loadingError';
export const ALERT_EVENT = 'alert';
+export const KEYDOWN_EVENT = 'keydown';
export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
@@ -66,3 +64,5 @@ export const SAFE_VIDEO_EXT = ['mp4', 'm4v', 'mov', 'webm', 'ogv'];
export const SAFE_AUDIO_EXT = ['mp3', 'oga', 'ogg', 'spx', 'wav'];
export const DIAGRAM_LANGUAGES = ['plantuml', 'mermaid'];
+
+export const TIPTAP_AUTOFOCUS_OPTIONS = [true, false, 'start', 'end', 'all'];
diff --git a/app/assets/javascripts/content_editor/extensions/diagram.js b/app/assets/javascripts/content_editor/extensions/diagram.js
index d9983b8c1c5..7c4a56468eb 100644
--- a/app/assets/javascripts/content_editor/extensions/diagram.js
+++ b/app/assets/javascripts/content_editor/extensions/diagram.js
@@ -1,5 +1,6 @@
import { lowlight } from 'lowlight/lib/core';
import { textblockTypeInputRule } from '@tiptap/core';
+import { base64DecodeUnicode } from '~/lib/utils/text_utility';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import languageLoader from '../services/code_block_language_loader';
import CodeBlockHighlight from './code_block_highlight';
@@ -45,7 +46,9 @@ export default CodeBlockHighlight.extend({
priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: '[data-diagram]',
getContent(element, schema) {
- const source = atob(element.dataset.diagramSrc.replace('data:text/plain;base64,', ''));
+ const source = base64DecodeUnicode(
+ element.dataset.diagramSrc.replace('data:text/plain;base64,', ''),
+ );
const node = schema.node('paragraph', {}, [schema.text(source)]);
return node.content;
},
diff --git a/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js b/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js
new file mode 100644
index 00000000000..e940614083e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/external_keydown_handler.js
@@ -0,0 +1,38 @@
+import { Extension } from '@tiptap/core';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { KEYDOWN_EVENT } from '../constants';
+
+/**
+ * This extension bubbles up the keydown event, captured by ProseMirror in the
+ * contenteditale element, to the presentation layer implemented in vue.
+ *
+ * The purpose of this mechanism is allowing clients of the
+ * content editor to attach keyboard shortcuts for behavior outside
+ * of the Content Editor’s boundaries, i.e. submitting a form to save changes.
+ */
+export default Extension.create({
+ name: 'keyboardShortcut',
+ addOptions() {
+ return {
+ eventHub: null,
+ };
+ },
+ addProseMirrorPlugins() {
+ return [
+ new Plugin({
+ key: new PluginKey('keyboardShortcut'),
+ props: {
+ handleKeyDown: (_, event) => {
+ const {
+ options: { eventHub },
+ } = this;
+
+ eventHub.$emit(KEYDOWN_EVENT, event);
+
+ return false;
+ },
+ },
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/heading.js b/app/assets/javascripts/content_editor/extensions/heading.js
index 48303cdeca4..41903162ba5 100644
--- a/app/assets/javascripts/content_editor/extensions/heading.js
+++ b/app/assets/javascripts/content_editor/extensions/heading.js
@@ -1 +1,15 @@
-export { Heading as default } from '@tiptap/extension-heading';
+import { Heading } from '@tiptap/extension-heading';
+import { textblockTypeInputRule } from '@tiptap/core';
+
+export default Heading.extend({
+ addInputRules() {
+ return this.options.levels.map((level) => {
+ return textblockTypeInputRule({
+ // make sure heading regex doesn't conflict with issue references
+ find: new RegExp(`^(#{1,${level}})[ \t]$`),
+ type: this.type,
+ getAttributes: { level },
+ });
+ });
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index 5e459e65de2..707beaf1231 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -46,22 +46,10 @@ export default Node.create({
tag: 'a.gfm:not([data-link=true])',
priority: PARSE_HTML_PRIORITY_HIGHEST,
},
- {
- tag: 'span.gl-label',
- },
];
},
renderHTML({ node }) {
- return [
- 'a',
- {
- class: node.attrs.className,
- href: node.attrs.href,
- 'data-reference-type': node.attrs.referenceType,
- 'data-original': node.attrs.originalText,
- },
- node.attrs.text,
- ];
+ return ['a', { href: '#' }, node.attrs.text];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js
new file mode 100644
index 00000000000..716e191c3d5
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/reference_label.js
@@ -0,0 +1,35 @@
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import LabelWrapper from '../components/wrappers/label.vue';
+import Reference from './reference';
+
+export default Reference.extend({
+ name: 'reference_label',
+
+ addAttributes() {
+ return {
+ ...this.parent(),
+ text: {
+ default: null,
+ parseHTML: (element) => {
+ const text = element.querySelector('.gl-label-text').textContent;
+ const scopedText = element.querySelector('.gl-label-text-scoped')?.textContent;
+ if (!scopedText) return text;
+ return `${text}${SCOPED_LABEL_DELIMITER}${scopedText}`;
+ },
+ },
+ color: {
+ default: null,
+ parseHTML: (element) => element.querySelector('.gl-label-text').style.backgroundColor,
+ },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'span.gl-label' }];
+ },
+
+ addNodeView() {
+ return new VueNodeViewRenderer(LabelWrapper);
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
new file mode 100644
index 00000000000..8976b9cafee
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -0,0 +1,227 @@
+import { Node } from '@tiptap/core';
+import { VueRenderer } from '@tiptap/vue-2';
+import tippy from 'tippy.js';
+import Suggestion from '@tiptap/suggestion';
+import { PluginKey } from 'prosemirror-state';
+import { isFunction, uniqueId, memoize } from 'lodash';
+import axios from '~/lib/utils/axios_utils';
+import { initEmojiMap, getAllEmoji } from '~/emoji';
+import SuggestionsDropdown from '../components/suggestions_dropdown.vue';
+
+function find(haystack, needle) {
+ return String(haystack).toLocaleLowerCase().includes(String(needle).toLocaleLowerCase());
+}
+
+function createSuggestionPlugin({
+ editor,
+ char,
+ dataSource,
+ search,
+ limit = Infinity,
+ nodeType,
+ nodeProps = {},
+}) {
+ const fetchData = memoize(
+ isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
+ );
+
+ return Suggestion({
+ editor,
+ char,
+ pluginKey: new PluginKey(uniqueId('suggestions')),
+
+ command: ({ editor: tiptapEditor, range, props }) => {
+ tiptapEditor
+ .chain()
+ .focus()
+ .insertContentAt(range, [
+ { type: nodeType, attrs: props },
+ { type: 'text', text: ' ' },
+ ])
+ .run();
+ },
+
+ async items({ query }) {
+ if (!dataSource) return [];
+
+ try {
+ const items = await fetchData();
+
+ return items.filter(search(query)).slice(0, limit);
+ } catch {
+ return [];
+ }
+ },
+
+ render: () => {
+ let component;
+ let popup;
+
+ return {
+ onStart: (props) => {
+ component = new VueRenderer(SuggestionsDropdown, {
+ propsData: {
+ ...props,
+ char,
+ nodeType,
+ nodeProps,
+ },
+ editor: props.editor,
+ });
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ popup = tippy('body', {
+ getReferenceClientRect: props.clientRect,
+ appendTo: () => document.body,
+ content: component.element,
+ showOnCreate: true,
+ interactive: true,
+ trigger: 'manual',
+ placement: 'bottom-start',
+ });
+ },
+
+ onUpdate(props) {
+ component?.updateProps(props);
+
+ if (!props.clientRect) {
+ return;
+ }
+
+ popup?.[0].setProps({
+ getReferenceClientRect: props.clientRect,
+ });
+ },
+
+ onKeyDown(props) {
+ if (props.event.key === 'Escape') {
+ popup?.[0].hide();
+
+ return true;
+ }
+
+ return component?.ref?.onKeyDown(props);
+ },
+
+ onExit() {
+ popup?.[0].destroy();
+ component?.destroy();
+ },
+ };
+ },
+ });
+}
+
+export default Node.create({
+ name: 'suggestions',
+
+ addProseMirrorPlugins() {
+ return [
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '@',
+ dataSource: gl.GfmAutoComplete?.dataSources.members,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'user',
+ },
+ search: (query) => ({ name, username }) => find(name, query) || find(username, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '#',
+ dataSource: gl.GfmAutoComplete?.dataSources.issues,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'issue',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '$',
+ dataSource: gl.GfmAutoComplete?.dataSources.snippets,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'snippet',
+ },
+ search: (query) => ({ id, title }) => find(id, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '~',
+ dataSource: gl.GfmAutoComplete?.dataSources.labels,
+ nodeType: 'reference_label',
+ nodeProps: {
+ referenceType: 'label',
+ },
+ search: (query) => ({ title }) => find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '&',
+ dataSource: gl.GfmAutoComplete?.dataSources.epics,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'epic',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '[vulnerability:',
+ dataSource: gl.GfmAutoComplete?.dataSources.vulnerabilities,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'vulnerability',
+ },
+ search: (query) => ({ id, title }) => find(id, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '!',
+ dataSource: gl.GfmAutoComplete?.dataSources.mergeRequests,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'merge_request',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '%',
+ dataSource: gl.GfmAutoComplete?.dataSources.milestones,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'milestone',
+ },
+ search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: '/',
+ dataSource: gl.GfmAutoComplete?.dataSources.commands,
+ nodeType: 'reference',
+ nodeProps: {
+ referenceType: 'command',
+ },
+ search: (query) => ({ name }) => find(name, query),
+ }),
+ createSuggestionPlugin({
+ editor: this.editor,
+ char: ':',
+ dataSource: () => Object.values(getAllEmoji()),
+ nodeType: 'emoji',
+ search: (query) => ({ d, name }) => find(d, query) || find(name, query),
+ limit: 10,
+ }),
+ ];
+ },
+
+ onCreate() {
+ initEmojiMap();
+ },
+});
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 5ed7f3dc23d..0d78390e769 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -18,6 +18,7 @@ import Diagram from '../extensions/diagram';
import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
+import ExternalKeydownHandler from '../extensions/external_keydown_handler';
import Figure from '../extensions/figure';
import FigureCaption from '../extensions/figure_caption';
import FootnoteDefinition from '../extensions/footnote_definition';
@@ -42,10 +43,12 @@ import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import PasteMarkdown from '../extensions/paste_markdown';
import Reference from '../extensions/reference';
+import ReferenceLabel from '../extensions/reference_label';
import ReferenceDefinition from '../extensions/reference_definition';
import Sourcemap from '../extensions/sourcemap';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
+import Suggestions from '../extensions/suggestions';
import Superscript from '../extensions/superscript';
import Table from '../extensions/table';
import TableCell from '../extensions/table_cell';
@@ -121,6 +124,7 @@ export const createContentEditor = ({
Image,
InlineDiff,
Italic,
+ ExternalKeydownHandler.configure({ eventHub }),
Link,
ListItem,
Loading,
@@ -129,10 +133,12 @@ export const createContentEditor = ({
Paragraph,
PasteMarkdown.configure({ eventHub, renderMarkdown }),
Reference,
+ ReferenceLabel,
ReferenceDefinition,
Sourcemap,
Strike,
Subscript,
+ Suggestions,
Superscript,
TableCell,
TableHeader,
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index ba0cad6c91c..c990f6cf0b3 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -33,6 +33,7 @@ import MathInline from '../extensions/math_inline';
import OrderedList from '../extensions/ordered_list';
import Paragraph from '../extensions/paragraph';
import Reference from '../extensions/reference';
+import ReferenceLabel from '../extensions/reference_label';
import ReferenceDefinition from '../extensions/reference_definition';
import Strike from '../extensions/strike';
import Subscript from '../extensions/subscript';
@@ -61,6 +62,7 @@ import {
renderHTMLNode,
renderContent,
renderBulletList,
+ renderReference,
preserveUnchanged,
bold,
italic,
@@ -184,9 +186,8 @@ const defaultSerializerConfig = {
[ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item),
[OrderedList.name]: preserveUnchanged(renderOrderedList),
[Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph),
- [Reference.name]: (state, node) => {
- state.write(node.attrs.originalText || node.attrs.text);
- },
+ [Reference.name]: renderReference,
+ [ReferenceLabel.name]: renderReference,
[ReferenceDefinition.name]: preserveUnchanged({
render: (state, node, parent, index, same, sourceMarkdown) => {
const nextSibling = parent.maybeChild(index + 1);
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 41114571df7..5c0cb21075a 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -280,6 +280,7 @@ export function renderTableRow(state, node) {
}
export function renderTable(state, node) {
+ state.flushClose();
setIsInBlockTable(node, shouldRenderHTMLTable(node));
if (isInBlockTable(node)) renderTagOpen(state, 'table');
@@ -422,6 +423,10 @@ export function renderOrderedList(state, node) {
});
}
+export function renderReference(state, node) {
+ state.write(node.attrs.originalText || node.attrs.text);
+}
+
const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 512f060e2ea..4e4c21328ca 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -196,7 +196,7 @@ export default {
<template>
<div>
- <div v-if="loading" class="contributors-loader text-center">
+ <div v-if="loading" class="gl-text-center gl-pt-13">
<gl-loading-icon :inline="true" size="xl" />
</div>
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
index 4cc0a6a6509..3a6f4191031 100644
--- a/app/assets/javascripts/contributors/stores/actions.js
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
@@ -14,7 +14,7 @@ export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_LOADING_STATE, false);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading chart data'),
}),
);
diff --git a/app/assets/javascripts/crm/contacts/components/contacts_root.vue b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
index 562363ff88e..2be17d1f80f 100644
--- a/app/assets/javascripts/crm/contacts/components/contacts_root.vue
+++ b/app/assets/javascripts/crm/contacts/components/contacts_root.vue
@@ -86,7 +86,7 @@ export default {
},
methods: {
errorAlertDismissed() {
- this.error = true;
+ this.error = false;
},
extractContacts(data) {
const contacts = data?.group?.contacts?.nodes || [];
@@ -146,7 +146,7 @@ export default {
editButtonLabel: __('Edit'),
title: s__('Crm|Customer relations contacts'),
newContact: s__('Crm|New contact'),
- errorText: __('Something went wrong. Please try again.'),
+ errorMsg: __('Something went wrong. Please try again.'),
},
EDIT_ROUTE_NAME,
NEW_ROUTE_NAME,
@@ -176,7 +176,7 @@ export default {
<div>
<paginated-table-with-search-and-tabs
:show-items="true"
- :show-error-msg="false"
+ :show-error-msg="error"
:i18n="$options.i18n"
:items="contacts.list"
:page-info="contacts.pageInfo"
@@ -243,10 +243,7 @@ export default {
</template>
<template #empty>
- <span v-if="error">
- {{ $options.i18n.errorText }}
- </span>
- <span v-else>
+ <span>
{{ $options.i18n.emptyText }}
</span>
</template>
diff --git a/app/assets/javascripts/crm/organizations/components/organizations_root.vue b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
index 155c8f00537..28f0b34f031 100644
--- a/app/assets/javascripts/crm/organizations/components/organizations_root.vue
+++ b/app/assets/javascripts/crm/organizations/components/organizations_root.vue
@@ -137,7 +137,7 @@ export default {
editButtonLabel: __('Edit'),
title: s__('Crm|Customer relations organizations'),
newOrganization: s__('Crm|New organization'),
- errorText: __('Something went wrong. Please try again.'),
+ errorMsg: __('Something went wrong. Please try again.'),
},
EDIT_ROUTE_NAME,
NEW_ROUTE_NAME,
@@ -167,7 +167,7 @@ export default {
<div>
<paginated-table-with-search-and-tabs
:show-items="true"
- :show-error-msg="false"
+ :show-error-msg="error"
:i18n="$options.i18n"
:items="organizations.list"
:page-info="organizations.pageInfo"
@@ -238,10 +238,7 @@ export default {
</template>
<template #empty>
- <span v-if="error">
- {{ $options.i18n.errorText }}
- </span>
- <span v-else>
+ <span>
{{ $options.i18n.emptyText }}
</span>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index 5c2e29bfa74..4a201e00582 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -6,7 +6,7 @@ import {
getValueStreamStageCounts,
} from '~/api/analytics_api';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
import * as types from './mutation_types';
@@ -97,7 +97,7 @@ export const fetchStageMedians = ({
.then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
- createFlash({ message: I18N_VSA_ERROR_STAGE_MEDIAN });
+ createAlert({ message: I18N_VSA_ERROR_STAGE_MEDIAN });
});
};
@@ -126,7 +126,7 @@ export const fetchStageCountValues = ({
.then((data) => commit(types.RECEIVE_STAGE_COUNTS_SUCCESS, data))
.catch((error) => {
commit(types.RECEIVE_STAGE_COUNTS_ERROR, error);
- createFlash({
+ createAlert({
message: __('There was an error fetching stage total counts'),
});
});
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
index 814a4b672a2..c67b544eacd 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue
@@ -3,7 +3,7 @@ import { GlFormGroup, GlFormInput, GlModal, GlSprintf, GlLink } from '@gitlab/ui
import { isValidCron } from 'cron-validator';
import { mapActions, mapState } from 'vuex';
import { __ } from '~/locale';
-import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import { mapComputed } from '~/vuex_shared/bindings';
export default {
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index 1ac6781a0e3..76a4eaaff3f 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -27,7 +27,7 @@ const receiveFreezePeriod = (store, request) => {
dispatch('fetchFreezePeriods');
})
.catch((error) => {
- createFlash({
+ createAlert({
message: __('Error: Unable to create deploy freeze'),
});
dispatch('receiveFreezePeriodError', error);
@@ -59,7 +59,7 @@ export const deleteFreezePeriod = ({ state, commit }, { id }) => {
return Api.deleteFreezePeriod(state.projectId, id)
.then(() => commit(types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS, id))
.catch((e) => {
- createFlash({
+ createAlert({
message: __('Error: Unable to delete deploy freeze'),
});
commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
@@ -76,7 +76,7 @@ export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.RECEIVE_FREEZE_PERIODS_SUCCESS, data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('There was an error fetching the deploy freezes.'),
});
});
diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js
index 151f7f39f5a..edb455a8dd5 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutations.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutations.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
const formatTimezoneName = (freezePeriod, timezoneList) => {
@@ -8,7 +8,7 @@ const formatTimezoneName = (freezePeriod, timezoneList) => {
return convertObjectPropsToCamelCase({
...freezePeriod,
cron_timezone: {
- formattedTimezone: tz && `[UTC ${secondsToHours(tz.offset)}] ${tz.name}`,
+ formattedTimezone: tz && formatTimezone(tz),
identifier: freezePeriod.cron_timezone,
},
});
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 36d54f586f1..db5e9a954cf 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
@@ -93,7 +93,7 @@ export default {
.catch(() => {
this.isLoading = false;
this.store.keys = {};
- return createFlash({
+ return createAlert({
message: s__('DeployKeys|Error getting deploy keys'),
});
});
@@ -103,7 +103,7 @@ export default {
.enableKey(deployKey.id)
.then(this.fetchKeys)
.catch(() =>
- createFlash({
+ createAlert({
message: s__('DeployKeys|Error enabling deploy key'),
}),
);
@@ -119,7 +119,7 @@ export default {
.then(this.fetchKeys)
.then(hideModal)
.catch(() =>
- createFlash({
+ createAlert({
message: s__('DeployKeys|Error removing deploy key'),
}),
);
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
new file mode 100644
index 00000000000..639dd21bd7b
--- /dev/null
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -0,0 +1,332 @@
+<script>
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlFormCheckbox,
+ GlButton,
+ GlDatepicker,
+ GlFormInputGroup,
+ GlSprintf,
+ GlLink,
+} from '@gitlab/ui';
+import { createAlert, VARIANT_INFO } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { s__ } from '~/locale';
+
+function defaultData() {
+ return {
+ expiresAt: null,
+ name: '',
+ newTokenDetails: null,
+ readRepository: false,
+ writeRepository: false,
+ readRegistry: false,
+ writeRegistry: false,
+ readPackageRegistry: false,
+ writePackageRegistry: false,
+ username: '',
+ placeholders: {
+ link: { link: ['link_start', 'link_end'] },
+ i: { i: ['i_start', 'i_end'] },
+ code: { code: ['code_start', 'code_end'] },
+ },
+ };
+}
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ GlDatepicker,
+ GlFormCheckbox,
+ GlButton,
+ GlFormInputGroup,
+ ClipboardButton,
+ GlSprintf,
+ GlLink,
+ },
+
+ props: {
+ createNewTokenPath: {
+ type: String,
+ required: true,
+ },
+ deployTokensHelpUrl: {
+ type: String,
+ required: true,
+ },
+ containerRegistryEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ packagesRegistryEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ tokenType: {
+ type: String,
+ required: true,
+ },
+ },
+
+ data() {
+ return defaultData();
+ },
+ translations: {
+ addTokenButton: s__('DeployTokens|Create deploy token'),
+ addTokenExpiryLabel: s__('DeployTokens|Expiration date (optional)'),
+ addTokenExpiryDescription: s__(
+ 'DeployTokens|Enter an expiration date for your token. Defaults to never expire.',
+ ),
+ addTokenHeader: s__('DeployTokens|New deploy token'),
+ addTokenDescription: s__(
+ 'DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}',
+ ),
+ addTokenNameLabel: s__('DeployTokens|Name'),
+ addTokenNameDescription: s__('DeployTokens|Enter a unique name for your deploy token.'),
+ addTokenScopesLabel: s__('DeployTokens|Scopes (select at least one)'),
+ addTokenUsernameDescription: s__(
+ 'DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.',
+ ),
+ addTokenUsernameLabel: s__('DeployTokens|Username (optional)'),
+ newTokenCopyMessage: s__('DeployTokens|Copy deploy token'),
+ newProjectTokenCreated: s__('DeployTokens|Your new project deploy token has been created.'),
+ newGroupTokenCreated: s__('DeployTokens|Your new group deploy token has been created.'),
+ newTokenDescription: s__(
+ 'DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered.',
+ ),
+ newTokenMessage: s__('DeployTokens|Your New Deploy Token'),
+ newTokenUsernameCopy: s__('DeployTokens|Copy username'),
+ newTokenUsernameDescription: s__(
+ 'DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}',
+ ),
+ readRepositoryHelp: s__('DeployTokens|Allows read-only access to the repository.'),
+ readRegistryHelp: s__('DeployTokens|Allows read-only access to registry images.'),
+ writeRegistryHelp: s__('DeployTokens|Allows read and write access to registry images.'),
+ readPackageRegistryHelp: s__('DeployTokens|Allows read-only access to the package registry.'),
+ writePackageRegistryHelp: s__(
+ 'DeployTokens|Allows read and write access to the package registry.',
+ ),
+ },
+ computed: {
+ formattedExpiryDate() {
+ return formatDate(this.expiresAt, 'yyyy-mm-dd');
+ },
+ newTokenCreatedMessage() {
+ return this.tokenType === 'group'
+ ? this.$options.translations.newGroupTokenCreated
+ : this.$options.translations.newProjectTokenCreated;
+ },
+ },
+ methods: {
+ createDeployToken() {
+ return axios
+ .post(this.createNewTokenPath, {
+ deploy_token: {
+ expires_at: this.expiresAt,
+ name: this.name,
+ read_repository: this.readRepository,
+ read_registry: this.readRegistry,
+ username: this.username,
+ },
+ })
+ .then((response) => {
+ this.newTokenDetails = response.data;
+ this.resetData();
+ createAlert({
+ variant: VARIANT_INFO,
+ message: this.newTokenCreatedMessage,
+ });
+ })
+ .catch((error) => {
+ createAlert({
+ message: error.response.data.message,
+ });
+ });
+ },
+ resetData() {
+ const newData = defaultData();
+ delete newData.newTokenDetails;
+ Object.keys(newData).forEach((k) => {
+ this[k] = newData[k];
+ });
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="newTokenDetails" class="created-deploy-token-container info-well">
+ <div class="well-segment">
+ <h5>{{ $options.translations.newTokenMessage }}</h5>
+ <gl-form-group>
+ <template #description>
+ <div class="deploy-token-help-block gl-mt-2 text-success">
+ <gl-sprintf
+ :message="$options.translations.newTokenUsernameDescription"
+ :placeholders="placeholders.link"
+ >
+ <template #link="{ content }">
+ <gl-link :href="deployTokensHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <gl-form-input-group
+ name="deploy-token-user"
+ :value="newTokenDetails.username"
+ select-on-click
+ readonly
+ >
+ <template #append>
+ <clipboard-button
+ :text="newTokenDetails.username"
+ :title="$options.translations.newTokenUsernameCopy"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+ <gl-form-group>
+ <template #description>
+ <div class="deploy-token-help-block gl-mt-2 text-danger">
+ <gl-sprintf
+ :message="$options.translations.newTokenDescription"
+ :placeholders="placeholders.i"
+ >
+ <template #i="{ content }">
+ <i>{{ content }}</i>
+ </template>
+ </gl-sprintf>
+ </div>
+ </template>
+ <gl-form-input-group :value="newTokenDetails.token" name="deploy-token" readonly>
+ <template #append>
+ <clipboard-button
+ :text="newTokenDetails.token"
+ :title="$options.translations.newTokenCopyMessage"
+ />
+ </template>
+ </gl-form-input-group>
+ </gl-form-group>
+ </div>
+ </div>
+ <h5>{{ $options.translations.addTokenHeader }}</h5>
+ <p class="profile-settings-content">
+ <gl-sprintf
+ :message="$options.translations.addTokenDescription"
+ :placeholders="placeholders.link"
+ >
+ <template #link="{ content }">
+ <gl-link :href="deployTokensHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-form-group
+ :label="$options.translations.addTokenNameLabel"
+ :description="$options.translations.addTokenNameDescription"
+ label-for="deploy_token_name"
+ >
+ <gl-form-input
+ id="deploy_token_name"
+ v-model="name"
+ name="deploy_token_name"
+ class="qa-deploy-token-name"
+ data-qa-selector="deploy_token_name_field"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.translations.addTokenExpiryLabel"
+ :description="$options.translations.addTokenExpiryDescription"
+ label-for="deploy_token_expires_at"
+ >
+ <gl-form-input
+ id="deploy_token_expires_at"
+ name="deploy_token_expires_at"
+ :value="formattedExpiryDate"
+ data-qa-selector="deploy_token_expires_at_field"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.translations.addTokenUsernameLabel"
+ label-for="deploy_token_username"
+ >
+ <template #description>
+ <gl-sprintf
+ :message="$options.translations.addTokenUsernameDescription"
+ :placeholders="placeholders.code"
+ >
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-input id="deploy_token_username" v-model="username" />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.translations.addTokenScopesLabel"
+ label-for="deploy-token-scopes"
+ >
+ <div id="deploy-token-scopes">
+ <!-- eslint-disable @gitlab/vue-require-i18n-strings -->
+ <gl-form-checkbox
+ id="deploy_token_read_repository"
+ v-model="readRepository"
+ name="deploy_token_read_repository"
+ data-qa-selector="deploy_token_read_repository_checkbox"
+ >
+ read_repository
+ <template #help>{{ $options.translations.readRepositoryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="containerRegistryEnabled"
+ id="deploy_token_read_registry"
+ v-model="readRegistry"
+ name="deploy_token_read_registry"
+ data-qa-selector="deploy_token_read_registry_checkbox"
+ >
+ read_registry
+ <template #help>{{ $options.translations.readRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="containerRegistryEnabled"
+ id="deploy_token_write_registry"
+ v-model="writeRegistry"
+ name="deploy_token_write_registry"
+ data-qa-selector="deploy_token_write_registry_checkbox"
+ >
+ write_registry
+ <template #help>{{ $options.translations.writeRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="packagesRegistryEnabled"
+ id="deploy_token_read_package_registry"
+ v-model="readPackageRegistry"
+ name="deploy_token_read_package_registry"
+ data-qa-selector="deploy_token_read_package_registry_checkbox"
+ >
+ read_package_registry
+ <template #help>{{ $options.translations.readPackageRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <gl-form-checkbox
+ v-if="packagesRegistryEnabled"
+ id="deploy_token_write_package_registry"
+ v-model="writePackageRegistry"
+ name="deploy_token_write_package_registry"
+ data-qa-selector="deploy_token_write_package_registry_checkbox"
+ >
+ write_package_registry
+ <template #help>{{ $options.translations.writePackageRegistryHelp }}</template>
+ </gl-form-checkbox>
+ <!-- eslint-enable @gitlab/vue-require-i18n-strings -->
+ </div>
+ </gl-form-group>
+ <div>
+ <gl-button variant="success" @click="createDeployToken">
+ {{ $options.translations.addTokenButton }}
+ </gl-button>
+ </div>
+ <gl-datepicker v-model="expiresAt" target="#deploy_token_expires_at" container="body" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/deploy_tokens/index.js b/app/assets/javascripts/deploy_tokens/index.js
new file mode 100644
index 00000000000..334c9930f4b
--- /dev/null
+++ b/app/assets/javascripts/deploy_tokens/index.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import NewDeployToken from './components/new_deploy_token.vue';
+
+export default function initDeployTokens() {
+ const el = document.getElementById('js-new-deploy-token');
+
+ if (el == null) return null;
+
+ const {
+ createNewTokenPath,
+ deployTokensHelpUrl,
+ containerRegistryEnabled,
+ packagesRegistryEnabled,
+ tokenType,
+ } = el.dataset;
+ return new Vue({
+ el,
+ components: {
+ NewDeployToken,
+ },
+ render(createElement) {
+ return createElement(NewDeployToken, {
+ props: {
+ createNewTokenPath,
+ deployTokensHelpUrl,
+ containerRegistryEnabled: containerRegistryEnabled !== undefined,
+ packagesRegistryEnabled: packagesRegistryEnabled !== undefined,
+ tokenType,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index 124780df8a5..a4430b15752 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
@@ -155,7 +155,7 @@ export default {
methods: {
onDone({ data: { createNote } }) {
if (hasErrors(createNote)) {
- createFlash({ message: ADD_DISCUSSION_COMMENT_ERROR });
+ createAlert({ message: ADD_DISCUSSION_COMMENT_ERROR });
}
this.discussionComment = '';
this.hideForm();
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 4faeba3983b..5a6b220e532 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,5 +1,5 @@
<script>
-import { GlButton, GlModal } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
@@ -7,13 +7,23 @@ import Autosave from '~/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
export default {
name: 'DesignReplyForm',
+ i18n: {
+ primaryBtn: s__('DesignManagement|Discard changes'),
+ cancelBtnCreate: s__('DesignManagement|Continue creating'),
+ cancelBtnUpdate: s__('DesignManagement|Continue editing'),
+ cancelCreate: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
+ cancelUpdate: s__('DesignManagement|Are you sure you want to cancel editing this comment?'),
+ newCommentButton: s__('DesignManagement|Comment'),
+ updateCommentButton: s__('DesignManagement|Save comment'),
+ },
+ markdownDocsPath: helpPagePath('user/markdown'),
components: {
MarkdownField,
GlButton,
- GlModal,
},
props: {
markdownPreviewPath: {
@@ -54,29 +64,10 @@ export default {
hasValue() {
return this.value.trim().length > 0;
},
- modalSettings() {
- if (this.isNewComment) {
- return {
- title: s__('DesignManagement|Cancel comment confirmation'),
- okTitle: s__('DesignManagement|Discard comment'),
- cancelTitle: s__('DesignManagement|Keep comment'),
- content: s__('DesignManagement|Are you sure you want to cancel creating this comment?'),
- };
- }
- return {
- title: s__('DesignManagement|Cancel comment update confirmation'),
- okTitle: s__('DesignManagement|Cancel changes'),
- cancelTitle: s__('DesignManagement|Keep changes'),
- content: s__('DesignManagement|Are you sure you want to cancel changes to this comment?'),
- };
- },
buttonText() {
return this.isNewComment
- ? s__('DesignManagement|Comment')
- : s__('DesignManagement|Save comment');
- },
- markdownDocsPath() {
- return helpPagePath('user/markdown');
+ ? this.$options.i18n.newCommentButton
+ : this.$options.i18n.updateCommentButton;
},
shortDiscussionId() {
return isGid(this.discussionId) ? getIdFromGraphQLId(this.discussionId) : this.discussionId;
@@ -94,12 +85,30 @@ export default {
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
- this.$refs.cancelCommentModal.show();
+ this.confirmCancelCommentModal();
} else {
this.$emit('cancel-form');
}
},
- confirmCancelCommentModal() {
+ async confirmCancelCommentModal() {
+ const msg = this.isNewComment
+ ? this.$options.i18n.cancelCreate
+ : this.$options.i18n.cancelUpdate;
+
+ const cancelBtn = this.isNewComment
+ ? this.$options.i18n.cancelBtnCreate
+ : this.$options.i18n.cancelBtnUpdate;
+
+ const confirmed = await confirmAction(msg, {
+ primaryBtnText: this.$options.i18n.primaryBtn,
+ cancelBtnText: cancelBtn,
+ primaryBtnVariant: 'danger',
+ });
+
+ if (!confirmed) {
+ return;
+ }
+
this.$emit('cancel-form');
this.autosaveDiscussion.reset();
},
@@ -126,7 +135,7 @@ export default {
:markdown-preview-path="markdownPreviewPath"
:enable-autocomplete="true"
:textarea-value="value"
- :markdown-docs-path="markdownDocsPath"
+ :markdown-docs-path="$options.markdownDocsPath"
class="bordered-box"
>
<template #textarea>
@@ -171,15 +180,5 @@ export default {
>{{ __('Cancel') }}</gl-button
>
</div>
- <gl-modal
- ref="cancelCommentModal"
- ok-variant="danger"
- :title="modalSettings.title"
- :ok-title="modalSettings.okTitle"
- :cancel-title="modalSettings.cancelTitle"
- modal-id="cancel-comment-modal"
- @ok="confirmCancelCommentModal"
- >{{ modalSettings.content }}
- </gl-modal>
</form>
</template>
diff --git a/app/assets/javascripts/design_management/mixins/all_designs.js b/app/assets/javascripts/design_management/mixins/all_designs.js
index e92f8006a0d..b783ec43cd1 100644
--- a/app/assets/javascripts/design_management/mixins/all_designs.js
+++ b/app/assets/javascripts/design_management/mixins/all_designs.js
@@ -1,6 +1,6 @@
import { propertyOf } from 'lodash';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { s__ } from '~/locale';
import { DESIGNS_ROUTE_NAME } from '../router/constants';
import allVersionsMixin from './all_versions';
@@ -36,7 +36,7 @@ export default {
},
result() {
if (this.$route.query.version && !this.hasValidVersion) {
- createFlash({
+ createAlert({
message: s__(
'DesignManagement|Requested design version does not exist. Showing latest version instead',
),
@@ -44,11 +44,11 @@ export default {
this.$router.replace({ name: DESIGNS_ROUTE_NAME, query: { version: undefined } });
}
if (this.designCollection.copyState === 'ERROR') {
- createFlash({
+ createAlert({
message: s__(
'DesignManagement|There was an error moving your designs. Please upload your designs below.',
),
- type: FLASH_TYPES.WARNING,
+ variant: VARIANT_WARNING,
});
}
},
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 228ad637b9e..d4c177e2e5f 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -4,7 +4,7 @@ import { isNull } from 'lodash';
import Mousetrap from 'mousetrap';
import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -250,7 +250,7 @@ export default {
onQueryError(message) {
// because we redirect user to /designs (the issue page),
// we want to create these flashes on the issue page
- createFlash({ message });
+ createAlert({ message });
this.$router.push({ name: this.$options.DESIGNS_ROUTE_NAME });
},
onError(message, e) {
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index 07f7a19f7d4..fba73cd4bec 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -133,9 +133,13 @@ export default {
return this.designCollection && this.designCollection.copyState === 'IN_PROGRESS';
},
designDropzoneWrapperClass() {
- return this.isDesignListEmpty
- ? 'col-12'
- : 'gl-flex-direction-column col-md-6 col-lg-3 gl-mt-5';
+ if (!this.isDesignListEmpty) {
+ return 'gl-flex-direction-column col-md-6 col-lg-3 gl-mt-5';
+ }
+ if (this.showToolbar) {
+ return 'col-12 gl-mt-5';
+ }
+ return 'col-12';
},
},
mounted() {
diff --git a/app/assets/javascripts/design_management/utils/cache_update.js b/app/assets/javascripts/design_management/utils/cache_update.js
index c8f445bfb88..cfec5828c85 100644
--- a/app/assets/javascripts/design_management/utils/cache_update.js
+++ b/app/assets/javascripts/design_management/utils/cache_update.js
@@ -2,7 +2,7 @@
import produce from 'immer';
import { differenceBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { extractCurrentDiscussion, extractDesign, extractDesigns } from './design_management_utils';
import {
ADD_IMAGE_DIFF_NOTE_ERROR,
@@ -234,7 +234,7 @@ export const deletePendingTodoFromStore = (store, todoMarkDone, query, queryVari
};
const onError = (data, message) => {
- createFlash({ message });
+ createAlert({ message });
throw new Error(data.errors);
};
@@ -283,7 +283,7 @@ export const updateStoreAfterUploadDesign = (store, data, query) => {
export const updateDesignsOnStoreAfterReorder = (store, data, query) => {
if (hasErrors(data)) {
- createFlash({ message: data.errors[0] });
+ createAlert({ message: data.errors[0] });
} else {
moveDesignInStore(store, data, query);
}
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 833fbb8789e..23eb470503e 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { merge } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import FilesCommentButton from './files_comment_button';
@@ -82,7 +82,7 @@ export default class Diff {
.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading diff'),
}),
);
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index f5c0776ca35..bc49464a560 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -11,7 +11,7 @@ import {
MR_COMMITS_NEXT_COMMIT,
MR_COMMITS_PREVIOUS_COMMIT,
} from '~/behaviors/shortcuts/keybindings';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
import { helpPagePath } from '~/helpers/help_page_helper';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -471,8 +471,8 @@ export default {
},
fetchData(toggleTree = true) {
this.fetchDiffFilesMeta()
- .then(({ real_size }) => {
- this.diffFilesLength = parseInt(real_size, 10);
+ .then(({ real_size = 0 }) => {
+ this.diffFilesLength = parseInt(real_size, 10) || 0;
if (toggleTree) {
this.setTreeDisplay();
}
@@ -480,7 +480,7 @@ export default {
this.updateChangesTabCount();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
});
});
@@ -495,7 +495,7 @@ export default {
this.setDiscussions();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
});
});
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 0e5acd0928b..5a45797ed98 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -128,7 +128,7 @@ export default {
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="32"
- class="avatar-cell d-none d-sm-block"
+ class="avatar-cell d-none d-sm-block gl-my-2 gl-mr-4"
/>
</div>
<div
@@ -172,7 +172,7 @@ export default {
v-if="commit.description_html"
v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
- class="commit-row-description gl-mb-3 gl-text-body"
+ class="commit-row-description gl-mb-3 gl-text-body gl-white-space-pre-line"
></pre>
</div>
</li>
diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue
index b1a2b2a72ea..facfc553053 100644
--- a/app/assets/javascripts/diffs/components/commit_widget.vue
+++ b/app/assets/javascripts/diffs/components/commit_widget.vue
@@ -22,7 +22,7 @@ export default {
<template>
<div class="info-well mw-100 mx-0">
<div class="well-segment">
- <ul class="blob-commit-info">
+ <ul class="gl-list-style-none gl-m-0 gl-p-0">
<commit-item :commit="commit" :collapsible="collapsible" />
</ul>
</div>
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 8a5325cf218..6104a304fbd 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -80,7 +80,7 @@ export default {
<template>
<div class="mr-version-controls">
- <div class="mr-version-menus-container content-block">
+ <div class="mr-version-menus-container gl-px-5 gl-pt-3 gl-pb-2">
<gl-button
v-if="hasChanges"
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index 3082ba0f16f..b2098b9e82d 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
import * as utils from '../store/utils';
@@ -92,7 +92,7 @@ export default {
) {
this.loadMoreLines({ endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers })
.catch(() => {
- createFlash({
+ createAlert({
message: s__('Diffs|Something went wrong while fetching diff lines.'),
});
})
@@ -224,6 +224,7 @@ export default {
<button
v-if="showExpandDown"
:title="s__('Diffs|Next 20 lines')"
+ :aria-label="s__('Diffs|Next 20 lines')"
:disabled="loading.down"
type="button"
class="js-unfold-down gl-rounded-0 gl-border-0 diff-line-expand-button"
@@ -235,6 +236,7 @@ export default {
<button
v-if="lineCountBetween !== -1 && lineCountBetween < 20"
:title="s__('Diffs|Expand all lines')"
+ :aria-label="s__('Diffs|Expand all lines')"
:disabled="loading.all"
type="button"
class="js-unfold-all gl-rounded-0 gl-border-0 diff-line-expand-button"
@@ -246,6 +248,7 @@ export default {
<button
v-if="showExpandUp"
:title="s__('Diffs|Previous 20 lines')"
+ :aria-label="s__('Diffs|Previous 20 lines')"
:disabled="loading.up"
type="button"
class="js-unfold gl-rounded-0 gl-border-0 diff-line-expand-button"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index aec608007d5..422bf52a1fa 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -10,7 +10,7 @@ import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
@@ -309,7 +309,7 @@ export default {
})
.catch(() => {
idState.isLoadingCollapsedDiff = false;
- createFlash({
+ createAlert({
message: this.$options.i18n.genericError,
});
});
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 705b43a222d..91c3df39e32 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -281,7 +281,7 @@ export default {
'gl-z-dropdown-menu!': idState.moreActionsShown,
'is-sidebar-moved': glFeatures.movedMrSidebar,
}"
- class="js-file-title file-title file-title-flex-parent"
+ class="js-file-title file-title file-title-flex-parent gl-border"
data-qa-selector="file_title_container"
:data-qa-file-name="filePath"
@click.self="handleToggleFile"
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index f610ac979ca..7732badde34 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -108,7 +108,7 @@ export const mapParallel = (content) => (line) => {
...left,
renderDiscussion: hasExpandedDiscussionOnLeft,
hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line),
- lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'),
+ lineDrafts: content.draftsForLine(content.diffFile.file_hash, line, 'left'),
hasCommentForm: left.hasForm,
isConflictMarker:
line.left.type === CONFLICT_MARKER_OUR || line.left.type === CONFLICT_MARKER_THEIR,
@@ -123,7 +123,7 @@ export const mapParallel = (content) => (line) => {
hasExpandedDiscussionOnRight && right.type && right.type !== EXPANDED_LINE_TYPE,
),
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
- lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
+ lineDrafts: content.draftsForLine(content.diffFile.file_hash, line, 'right'),
hasCommentForm: Boolean(right.hasForm && right.type && right.type !== EXPANDED_LINE_TYPE),
emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR },
addCommentTooltip: addCommentTooltip(line.right),
@@ -145,7 +145,7 @@ export const mapParallel = (content) => (line) => {
lineCode: lineCode(line),
isMetaLineLeft: isMetaLine(left?.type),
isMetaLineRight: isMetaLine(right?.type),
- draftRowClasses: left?.lineDraft > 0 || right?.lineDraft > 0 ? '' : 'js-temp-notes-holder',
+ draftRowClasses: left?.hasDraft || right?.hasDraft ? '' : 'js-temp-notes-holder',
renderCommentRow,
commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder',
};
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 91bf3283379..5ea118afe78 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -177,6 +177,12 @@ export default {
getCodeQualityLine(line) {
return (line.left ?? line.right)?.codequality?.[0]?.line;
},
+ lineDrafts(line, side) {
+ return (line[side]?.lineDrafts || []).filter((entry) => entry.isDraft);
+ },
+ lineHasDrafts(line, side) {
+ return this.lineDrafts(line, side).length > 0;
+ },
},
userColorScheme: window.gon.user_color_scheme,
};
@@ -297,19 +303,19 @@ export default {
class="diff-grid-drafts diff-tr notes_holder"
>
<div
- v-if="!inline || (line.left && line.left.lineDraft.isDraft)"
+ v-if="!inline || lineHasDrafts(line, 'left')"
class="diff-td notes-content parallel old"
>
- <div v-if="line.left && line.left.lineDraft.isDraft" class="content">
- <draft-note :draft="line.left.lineDraft" :line="line.left" />
+ <div v-for="draft in lineDrafts(line, 'left')" :key="draft.id" class="content">
+ <draft-note :draft="draft" :line="line.left" />
</div>
</div>
<div
- v-if="!inline || (line.right && line.right.lineDraft.isDraft)"
+ v-if="!inline || lineHasDrafts(line, 'right')"
class="diff-td notes-content parallel new"
>
- <div v-if="line.right && line.right.lineDraft.isDraft" class="content">
- <draft-note :draft="line.right.lineDraft" :line="line.right" />
+ <div v-for="draft in lineDrafts(line, 'right')" :key="draft.id" class="content">
+ <draft-note :draft="draft" :line="line.right" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/mixins/draft_comments.js b/app/assets/javascripts/diffs/mixins/draft_comments.js
index 693b4a84694..d41bb160e96 100644
--- a/app/assets/javascripts/diffs/mixins/draft_comments.js
+++ b/app/assets/javascripts/diffs/mixins/draft_comments.js
@@ -5,7 +5,7 @@ export default {
...mapGetters('batchComments', [
'shouldRenderDraftRow',
'shouldRenderParallelDraftRow',
- 'draftForLine',
+ 'draftsForLine',
'draftsForFile',
'hasParallelDraftLeft',
'hasParallelDraftRight',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 5e74a7206b3..5234be44b05 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,7 +5,7 @@ import {
historyPushState,
scrollToElement,
} from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { diffViewerModes } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
@@ -202,6 +202,7 @@ export const fetchDiffFilesMeta = ({ commit, state }) => {
const worker = new TreeWorker();
const urlParams = {
view: 'inline',
+ w: state.showWhitespace ? '0' : '1',
};
commit(types.SET_LOADING, true);
@@ -246,7 +247,7 @@ export const fetchCoverageFiles = ({ commit, state }) => {
}
},
errorCallback: () =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
}),
});
@@ -509,7 +510,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() =>
- createFlash({
+ createAlert({
message: s__('MergeRequests|Saving the comment failed'),
}),
);
@@ -619,7 +620,7 @@ export const cacheTreeListWidth = (_, size) => {
export const receiveFullDiffError = ({ commit }, filePath) => {
commit(types.RECEIVE_FULL_DIFF_ERROR, filePath);
- createFlash({
+ createAlert({
message: s__('MergeRequest|Error loading full diff. Please try again.'),
});
};
@@ -757,7 +758,7 @@ export const setSuggestPopoverDismissed = ({ commit, state }) =>
commit(types.SET_SHOW_SUGGEST_POPOVER);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequest|Error dismissing suggestion popover. Please try again.'),
});
});
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index bc3cb163c39..999e91eed19 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -1,7 +1,7 @@
import { KeyMod, KeyCode } from 'monaco-editor';
import { debounce } from 'lodash';
import { BLOB_PREVIEW_ERROR } from '~/blob_edit/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import syntaxHighlight from '~/syntax_highlight';
@@ -152,7 +152,7 @@ export class EditorMarkdownPreviewExtension {
syntaxHighlight(previewEl.querySelectorAll('.js-syntax-highlight'));
previewEl.style.display = 'block';
})
- .catch(() => createFlash(BLOB_PREVIEW_ERROR));
+ .catch(() => createAlert(BLOB_PREVIEW_ERROR));
}
setupPreviewAction(instance) {
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 848ba7dbeef..e56932a9a31 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -1,7 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://gitlab.com/.gitlab-ci.yml",
- "title": "Gitlab CI configuration",
"markdownDescription": "Gitlab has a built-in solution for doing CI called Gitlab CI. It is configured by supplying a file called `.gitlab-ci.yml`, which will list all the jobs that are going to run for the project. A full list of all options can be found [here](https://docs.gitlab.com/ee/ci/yaml). [Learn More](https://docs.gitlab.com/ee/ci/index.html).",
"type": "object",
"properties": {
@@ -9,34 +8,74 @@
"type": "string",
"format": "uri"
},
- "image": { "$ref": "#/definitions/image" },
- "services": { "$ref": "#/definitions/services" },
- "before_script": { "$ref": "#/definitions/before_script" },
- "after_script": { "$ref": "#/definitions/after_script" },
- "variables": { "$ref": "#/definitions/globalVariables" },
- "cache": { "$ref": "#/definitions/cache" },
- "!reference": {"$ref" : "#/definitions/!reference"},
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "services": {
+ "$ref": "#/definitions/services"
+ },
+ "before_script": {
+ "$ref": "#/definitions/before_script"
+ },
+ "after_script": {
+ "$ref": "#/definitions/after_script"
+ },
+ "variables": {
+ "$ref": "#/definitions/globalVariables"
+ },
+ "cache": {
+ "$ref": "#/definitions/cache"
+ },
+ "!reference": {
+ "$ref": "#/definitions/!reference"
+ },
"default": {
"type": "object",
"properties": {
- "after_script": { "$ref": "#/definitions/after_script" },
- "artifacts": { "$ref": "#/definitions/artifacts" },
- "before_script": { "$ref": "#/definitions/before_script" },
- "cache": { "$ref": "#/definitions/cache" },
- "image": { "$ref": "#/definitions/image" },
- "interruptible": { "$ref": "#/definitions/interruptible" },
- "retry": { "$ref": "#/definitions/retry" },
- "services": { "$ref": "#/definitions/services" },
- "tags": { "$ref": "#/definitions/tags" },
- "timeout": { "$ref": "#/definitions/timeout" },
- "!reference": {"$ref" : "#/definitions/!reference"}
+ "after_script": {
+ "$ref": "#/definitions/after_script"
+ },
+ "artifacts": {
+ "$ref": "#/definitions/artifacts"
+ },
+ "before_script": {
+ "$ref": "#/definitions/before_script"
+ },
+ "cache": {
+ "$ref": "#/definitions/cache"
+ },
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "interruptible": {
+ "$ref": "#/definitions/interruptible"
+ },
+ "retry": {
+ "$ref": "#/definitions/retry"
+ },
+ "services": {
+ "$ref": "#/definitions/services"
+ },
+ "tags": {
+ "$ref": "#/definitions/tags"
+ },
+ "timeout": {
+ "$ref": "#/definitions/timeout"
+ },
+ "!reference": {
+ "$ref": "#/definitions/!reference"
+ }
},
"additionalProperties": false
},
"stages": {
"type": "array",
"markdownDescription": "Groups jobs into stages. All jobs in one stage must complete before next stage is executed. Defaults to ['build', 'test', 'deploy']. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#stages).",
- "default": ["build", "test", "deploy"],
+ "default": [
+ "build",
+ "test",
+ "deploy"
+ ],
"items": {
"type": "string"
},
@@ -46,10 +85,14 @@
"include": {
"markdownDescription": "Can be `IncludeItem` or `IncludeItem[]`. Each `IncludeItem` will be a string, or an object with properties for the method if including external YAML file. The external content will be fetched, included and evaluated along the `.gitlab-ci.yml`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#include).",
"oneOf": [
- { "$ref": "#/definitions/include_item" },
+ {
+ "$ref": "#/definitions/include_item"
+ },
{
"type": "array",
- "items": { "$ref": "#/definitions/include_item" }
+ "items": {
+ "$ref": "#/definitions/include_item"
+ }
}
]
},
@@ -60,21 +103,41 @@
"workflow": {
"type": "object",
"properties": {
+ "name": { "$ref": "#/definitions/workflowName" },
"rules": {
"type": "array",
"items": {
"anyOf": [
- {"type": "object"},
- {"type": "array", "minLength": 1, "items": { "type": "string" }}
+ {
+ "type": "object"
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
],
"properties": {
- "if": { "$ref": "#/definitions/if" },
- "changes": { "$ref": "#/definitions/changes" },
- "exists": { "$ref": "#/definitions/exists" },
- "variables": { "$ref": "#/definitions/variables" },
+ "if": {
+ "$ref": "#/definitions/if"
+ },
+ "changes": {
+ "$ref": "#/definitions/changes"
+ },
+ "exists": {
+ "$ref": "#/definitions/exists"
+ },
+ "variables": {
+ "$ref": "#/definitions/variables"
+ },
"when": {
"type": "string",
- "enum": ["always", "never"]
+ "enum": [
+ "always",
+ "never"
+ ]
}
},
"additionalProperties": false
@@ -87,8 +150,12 @@
"^[.]": {
"description": "Hidden keys.",
"anyOf": [
- { "$ref": "#/definitions/job_template" },
- { "description": "Arbitrary YAML anchor." }
+ {
+ "$ref": "#/definitions/job_template"
+ },
+ {
+ "description": "Arbitrary YAML anchor."
+ }
]
}
},
@@ -135,15 +202,21 @@
"default": "on_success",
"oneOf": [
{
- "enum": ["on_success"],
+ "enum": [
+ "on_success"
+ ],
"description": "Upload artifacts only when the job succeeds (this is the default)."
},
{
- "enum": ["on_failure"],
+ "enum": [
+ "on_failure"
+ ],
"description": "Upload artifacts only when the job fails."
},
{
- "enum": ["always"],
+ "enum": [
+ "always"
+ ],
"description": "Upload artifacts regardless of job status."
}
]
@@ -181,7 +254,9 @@
"properties": {
"coverage_format": {
"description": "Code coverage format used by the test framework.",
- "enum": ["cobertura"]
+ "enum": [
+ "cobertura"
+ ]
},
"path": {
"description": "Path to the coverage report file that should be parsed.",
@@ -285,18 +360,22 @@
"format": "uri-reference",
"pattern": "\\.ya?ml$"
},
- "rules": { "$ref": "#/definitions/rules" }
+ "rules": {
+ "$ref": "#/definitions/rules"
+ }
},
- "required": ["local"]
+ "required": [
+ "local"
+ ]
},
{
"type": "object",
"additionalProperties": false,
"properties": {
"project": {
- "description": "Path to the project, e.g. `group/project`, or `group/sub-group/project`.",
+ "description": "Path to the project, e.g. `group/project`, or `group/sub-group/project` [Learn more](https://docs.gitlab.com/ee/ci/yaml/index.html#includefile).",
"type": "string",
- "pattern": "\\S/\\S|\\$(\\S+)"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
},
"ref": {
"description": "Branch/Tag/Commit-hash for the target project.",
@@ -320,7 +399,10 @@
]
}
},
- "required": ["project", "file"]
+ "required": [
+ "project",
+ "file"
+ ]
},
{
"type": "object",
@@ -333,7 +415,9 @@
"pattern": "\\.ya?ml$"
}
},
- "required": ["template"]
+ "required": [
+ "template"
+ ]
},
{
"type": "object",
@@ -346,7 +430,9 @@
"pattern": "^https?://.+\\.ya?ml$"
}
},
- "required": ["remote"]
+ "required": [
+ "remote"
+ ]
}
]
},
@@ -407,7 +493,16 @@
]
}
},
- "required": ["name"]
+ "required": [
+ "name"
+ ]
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
}
],
"markdownDescription": "Specifies the docker image to use for the job or globally for all jobs. Job configuration takes precedence over global setting. Requires a certain kind of Gitlab runner executor. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#image)."
@@ -481,7 +576,9 @@
"minLength": 1
}
},
- "required": ["name"]
+ "required": [
+ "name"
+ ]
}
]
}
@@ -505,20 +602,37 @@
"engine": {
"type": "object",
"properties": {
- "name": { "type": "string" },
- "path": { "type": "string" }
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ }
},
- "required": ["name", "path"]
+ "required": [
+ "name",
+ "path"
+ ]
+ },
+ "path": {
+ "type": "string"
},
- "path": { "type": "string" },
- "field": { "type": "string" }
+ "field": {
+ "type": "string"
+ }
},
- "required": ["engine", "path", "field"]
+ "required": [
+ "engine",
+ "path",
+ "field"
+ ]
}
]
}
},
- "required": ["vault"]
+ "required": [
+ "vault"
+ ]
}
},
"before_script": {
@@ -564,45 +678,77 @@
"type": "object",
"additionalProperties": false,
"properties": {
- "if": { "$ref": "#/definitions/if" },
- "changes": { "$ref": "#/definitions/changes" },
- "exists": { "$ref": "#/definitions/exists" },
- "variables": { "$ref": "#/definitions/variables" },
- "when": { "$ref": "#/definitions/when" },
- "start_in": { "$ref": "#/definitions/start_in" },
- "allow_failure": { "$ref": "#/definitions/allow_failure" }
+ "if": {
+ "$ref": "#/definitions/if"
+ },
+ "changes": {
+ "$ref": "#/definitions/changes"
+ },
+ "exists": {
+ "$ref": "#/definitions/exists"
+ },
+ "variables": {
+ "$ref": "#/definitions/variables"
+ },
+ "when": {
+ "$ref": "#/definitions/when"
+ },
+ "start_in": {
+ "$ref": "#/definitions/start_in"
+ },
+ "allow_failure": {
+ "$ref": "#/definitions/allow_failure"
+ }
}
},
- {"type": "string", "minLength": 1},
- {"type": "array", "minLength": 1, "items": { "type": "string" }}
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
]
}
},
+ "workflowName": {
+ "type": "string",
+ "markdownDescription": "Defines the pipeline name. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowname).",
+ "minLength": 1,
+ "maxLength": 255
+ },
"globalVariables": {
- "markdownDescription": "Defines environment variables globally. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. You can use the value and description keywords to define variables that are prefilled when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
- "anyOf": [
- {"type": "object"},
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ],
- "additionalProperties": {
- "anyOf": [
- {"type": ["string", "integer", "array"]},
- {
- "type": "object",
- "properties": {
- "value": { "type": "string" },
- "description": {
- "type": "string",
- "description": "Explains what the variable is used for, what the acceptable values are."
- }
+ "markdownDescription": "Defines default variables for all jobs. Job level property overrides global variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "oneOf": [
+ {
+ "type": [
+ "string",
+ "number"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "markdownDescription": "Explains what the variable is used for, what the acceptable values are. Variables with `description` are prefilled when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variablesdescription)."
+ }
+ },
+ "additionalProperties": false
}
- }
- ]
+ ]
+ },
+ "additionalProperties": false
}
},
"if": {
@@ -615,7 +761,9 @@
{
"type": "object",
"additionalProperties": false,
- "required": ["paths"],
+ "required": [
+ "paths"
+ ],
"properties": {
"paths": {
"type": "array",
@@ -646,21 +794,17 @@
}
},
"variables": {
- "markdownDescription": "Defines environment variables for specific jobs. Job level property overrides global variables. If a job sets `variables: {}`, all global variables are turned off. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#rulesvariables).",
- "anyOf": [
- {
- "type": "object",
- "additionalProperties": {
- "type": ["string", "integer", "array"]
- }
+ "markdownDescription": "Defines environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
+ "type": "object",
+ "patternProperties": {
+ ".*": {
+ "type": [
+ "string",
+ "number"
+ ]
},
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- }
- ]
+ "additionalProperties": false
+ }
},
"timeout": {
"type": "string",
@@ -684,7 +828,9 @@
"description": "Exit code that are not considered failure. The job fails for any other exit code.",
"type": "object",
"additionalProperties": false,
- "required": ["exit_codes"],
+ "required": [
+ "exit_codes"
+ ],
"properties": {
"exit_codes": {
"type": "integer"
@@ -695,7 +841,9 @@
"description": "You can list which exit codes are not considered failures. The job fails for any other exit code.",
"type": "object",
"additionalProperties": false,
- "required": ["exit_codes"],
+ "required": [
+ "exit_codes"
+ ],
"properties": {
"exit_codes": {
"type": "array",
@@ -714,27 +862,39 @@
"default": "on_success",
"oneOf": [
{
- "enum": ["on_success"],
+ "enum": [
+ "on_success"
+ ],
"description": "Execute job only when all jobs from prior stages succeed."
},
{
- "enum": ["on_failure"],
+ "enum": [
+ "on_failure"
+ ],
"description": "Execute job when at least one job from prior stages fails."
},
{
- "enum": ["always"],
+ "enum": [
+ "always"
+ ],
"description": "Execute job regardless of the status from prior stages."
},
{
- "enum": ["manual"],
+ "enum": [
+ "manual"
+ ],
"markdownDescription": "Execute the job manually from Gitlab UI or API. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
},
{
- "enum": ["delayed"],
+ "enum": [
+ "delayed"
+ ],
"markdownDescription": "Execute a job after the time limit in 'start_in' expires. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#when)."
},
{
- "enum": ["never"],
+ "enum": [
+ "never"
+ ],
"description": "Never execute the job."
}
]
@@ -746,15 +906,21 @@
"default": "on_success",
"oneOf": [
{
- "enum": ["on_success"],
+ "enum": [
+ "on_success"
+ ],
"description": "Save the cache only when the job succeeds."
},
{
- "enum": ["on_failure"],
+ "enum": [
+ "on_failure"
+ ],
"description": "Save the cache only when the job fails. "
},
{
- "enum": ["always"],
+ "enum": [
+ "always"
+ ],
"description": "Always save the cache. "
}
]
@@ -806,15 +972,21 @@
"default": "pull-push",
"oneOf": [
{
- "enum": ["pull"],
+ "enum": [
+ "pull"
+ ],
"description": "Pull will download cache but skip uploading after job completes."
},
{
- "enum": ["push"],
+ "enum": [
+ "push"
+ ],
"description": "Push will skip downloading cache and always recreate cache after job completes."
},
{
- "enum": ["pull-push"],
+ "enum": [
+ "pull-push"
+ ],
"description": "Pull-push will both download cache at job start and upload cache on job success."
}
]
@@ -829,39 +1001,57 @@
{
"oneOf": [
{
- "enum": ["branches"],
+ "enum": [
+ "branches"
+ ],
"description": "When a branch is pushed."
},
{
- "enum": ["tags"],
+ "enum": [
+ "tags"
+ ],
"description": "When a tag is pushed."
},
{
- "enum": ["api"],
+ "enum": [
+ "api"
+ ],
"description": "When a pipeline has been triggered by a second pipelines API (not triggers API)."
},
{
- "enum": ["external"],
+ "enum": [
+ "external"
+ ],
"description": "When using CI services other than Gitlab"
},
{
- "enum": ["pipelines"],
+ "enum": [
+ "pipelines"
+ ],
"description": "For multi-project triggers, created using the API with 'CI_JOB_TOKEN'."
},
{
- "enum": ["pushes"],
+ "enum": [
+ "pushes"
+ ],
"description": "Pipeline is triggered by a `git push` by the user"
},
{
- "enum": ["schedules"],
+ "enum": [
+ "schedules"
+ ],
"description": "For scheduled pipelines."
},
{
- "enum": ["triggers"],
+ "enum": [
+ "triggers"
+ ],
"description": "For pipelines created using a trigger token."
},
{
- "enum": ["web"],
+ "enum": [
+ "web"
+ ],
"description": "For pipelines created using *Run pipeline* button in Gitlab UI (under your project's *Pipelines*)."
}
]
@@ -889,7 +1079,9 @@
"$ref": "#/definitions/filter_refs"
},
"kubernetes": {
- "enum": ["active"],
+ "enum": [
+ "active"
+ ],
"description": "Filter job based on if Kubernetes integration is active."
},
"variables": {
@@ -913,16 +1105,22 @@
"retry": {
"markdownDescription": "Retry a job if it fails. Can be a simple integer or object definition. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retry).",
"oneOf": [
- { "$ref": "#/definitions/retry_max" },
+ {
+ "$ref": "#/definitions/retry_max"
+ },
{
"type": "object",
"additionalProperties": false,
"properties": {
- "max": { "$ref": "#/definitions/retry_max" },
+ "max": {
+ "$ref": "#/definitions/retry_max"
+ },
"when": {
"markdownDescription": "Either a single or array of error types to trigger job retry. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#retrywhen).",
"oneOf": [
- { "$ref": "#/definitions/retry_errors" },
+ {
+ "$ref": "#/definitions/retry_errors"
+ },
{
"type": "array",
"items": {
@@ -1005,21 +1203,39 @@
},
"job": {
"allOf": [
- { "$ref": "#/definitions/job_template" }
+ {
+ "$ref": "#/definitions/job_template"
+ }
]
},
"job_template": {
"type": "object",
"additionalProperties": false,
"properties": {
- "image": { "$ref": "#/definitions/image" },
- "services": { "$ref": "#/definitions/services" },
- "before_script": { "$ref": "#/definitions/before_script" },
- "after_script": { "$ref": "#/definitions/after_script" },
- "rules": { "$ref": "#/definitions/rules" },
- "variables": { "$ref": "#/definitions/variables" },
- "cache": { "$ref": "#/definitions/cache" },
- "secrets": { "$ref": "#/definitions/secrets" },
+ "image": {
+ "$ref": "#/definitions/image"
+ },
+ "services": {
+ "$ref": "#/definitions/services"
+ },
+ "before_script": {
+ "$ref": "#/definitions/before_script"
+ },
+ "after_script": {
+ "$ref": "#/definitions/after_script"
+ },
+ "rules": {
+ "$ref": "#/definitions/rules"
+ },
+ "variables": {
+ "$ref": "#/definitions/variables"
+ },
+ "cache": {
+ "$ref": "#/definitions/cache"
+ },
+ "secrets": {
+ "$ref": "#/definitions/secrets"
+ },
"script": {
"markdownDescription": "Shell scripts executed by the Runner. The only required property of jobs. Be careful with special characters (e.g. `:`, `{`, `}`, `&`) and use single or double quotes to avoid issues. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#script)",
"oneOf": [
@@ -1047,9 +1263,20 @@
]
},
"stage": {
- "type": "string",
"description": "Define what stage the job will run in.",
- "minLength": 1
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
},
"only": {
"$ref": "#/definitions/filter",
@@ -1092,7 +1319,9 @@
"type": "boolean"
}
},
- "required": ["job"]
+ "required": [
+ "job"
+ ]
},
{
"type": "object",
@@ -1108,7 +1337,10 @@
"type": "boolean"
}
},
- "required": ["job", "pipeline"]
+ "required": [
+ "job",
+ "pipeline"
+ ]
},
{
"type": "object",
@@ -1127,7 +1359,11 @@
"type": "boolean"
}
},
- "required": ["job", "project", "ref"]
+ "required": [
+ "job",
+ "project",
+ "ref"
+ ]
}
]
}
@@ -1164,7 +1400,9 @@
"environment": {
"description": "Used to associate environment metadata with a deploy. Environment can have a name and URL attached to it, and will be displayed under /environments under the project.",
"oneOf": [
- { "type": "string" },
+ {
+ "type": "string"
+ },
{
"type": "object",
"additionalProperties": false,
@@ -1185,7 +1423,13 @@
"description": "The name of a job to execute when the environment is about to be stopped."
},
"action": {
- "enum": ["start", "prepare", "stop", "verify", "access"],
+ "enum": [
+ "start",
+ "prepare",
+ "stop",
+ "verify",
+ "access"
+ ],
"description": "Specifies what this job will do. 'start' (default) indicates the job will start the deployment. 'prepare'/'verify'/'access' indicates this will not affect the deployment. 'stop' indicates this will stop the deployment.",
"default": "start"
},
@@ -1216,7 +1460,9 @@
]
}
},
- "required": ["name"]
+ "required": [
+ "name"
+ ]
}
]
},
@@ -1296,15 +1542,23 @@
]
}
},
- "required": ["name", "url"]
+ "required": [
+ "name",
+ "url"
+ ]
},
"minItems": 1
}
},
- "required": ["links"]
+ "required": [
+ "links"
+ ]
}
},
- "required": ["tag_name", "description"]
+ "required": [
+ "tag_name",
+ "description"
+ ]
},
"coverage": {
"type": "string",
@@ -1335,14 +1589,20 @@
"type": "object",
"description": "Defines environment variables for specific job.",
"additionalProperties": {
- "type": ["string", "number", "array"]
+ "type": [
+ "string",
+ "number",
+ "array"
+ ]
}
},
"maxItems": 50
}
},
"additionalProperties": false,
- "required": ["matrix"]
+ "required": [
+ "matrix"
+ ]
}
]
},
@@ -1364,7 +1624,7 @@
"project": {
"description": "Path to the project, e.g. `group/project`, or `group/sub-group/project`.",
"type": "string",
- "pattern": "\\S/\\S"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
},
"branch": {
"description": "The branch name that a downstream pipeline will use",
@@ -1373,7 +1633,9 @@
"strategy": {
"description": "You can mirror the pipeline status from the triggered pipeline to the source bridge job by using strategy: depend",
"type": "string",
- "enum": ["depend"]
+ "enum": [
+ "depend"
+ ]
},
"forward": {
"description": "Specify what to forward to the downstream pipeline.",
@@ -1393,9 +1655,13 @@
}
}
},
- "required": ["project"],
+ "required": [
+ "project"
+ ],
"dependencies": {
- "branch": ["project"]
+ "branch": [
+ "project"
+ ]
}
},
{
@@ -1456,7 +1722,10 @@
"type": "string"
}
},
- "required": ["artifact", "job"]
+ "required": [
+ "artifact",
+ "job"
+ ]
},
{
"type": "object",
@@ -1465,7 +1734,7 @@
"project": {
"description": "Path to another private project under the same GitLab instance, like `group/project` or `group/sub-group/project`.",
"type": "string",
- "pattern": "\\S/\\S"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
},
"ref": {
"description": "Branch/Tag/Commit hash for the target project.",
@@ -1479,7 +1748,10 @@
"pattern": "\\.ya?ml$"
}
},
- "required": ["project", "file"]
+ "required": [
+ "project",
+ "file"
+ ]
}
]
}
@@ -1489,7 +1761,9 @@
"strategy": {
"description": "You can mirror the pipeline status from the triggered pipeline to the source bridge job by using strategy: depend",
"type": "string",
- "enum": ["depend"]
+ "enum": [
+ "depend"
+ ]
},
"forward": {
"description": "Specify what to forward to the downstream pipeline.",
@@ -1511,9 +1785,9 @@
}
},
{
- "markdownDescription": "Path to the project, e.g. `group/project`, or `group/sub-group/project`. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#define-multi-project-pipelines-in-your-gitlab-ciyml-file).",
+ "markdownDescription": "Path to the project, e.g. `group/project`, or `group/sub-group/project`. [Learn More](https://docs.gitlab.com/ee/ci/yaml/index.html#trigger).",
"type": "string",
- "pattern": "\\S/\\S"
+ "pattern": "(?:\\S/\\S|\\$\\S+)"
}
]
},
@@ -1550,7 +1824,9 @@
"variables": {
"markdownDescription": "Whether to inherit all globally-defined variables or not. Or subset of inherited variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#inheritvariables).",
"oneOf": [
- { "type": "boolean" },
+ {
+ "type": "boolean"
+ },
{
"type": "array",
"items": {
@@ -1566,15 +1842,24 @@
"oneOf": [
{
"properties": {
- "when": { "enum": ["delayed"] }
+ "when": {
+ "enum": [
+ "delayed"
+ ]
+ }
},
- "required": ["when", "start_in"]
+ "required": [
+ "when",
+ "start_in"
+ ]
},
{
"properties": {
"when": {
"not": {
- "enum": ["delayed"]
+ "enum": [
+ "delayed"
+ ]
}
}
}
@@ -1583,10 +1868,23 @@
},
"tags": {
"type": "array",
+ "minLength": 1,
"markdownDescription": "Used to select runners from the list of available runners. A runner must have all tags listed here to run the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#tags).",
"items": {
- "type": "string"
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "minLength": 1,
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
}
}
}
-}
+} \ No newline at end of file
diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue
index 3173c2bd644..78e1b8d5cb2 100644
--- a/app/assets/javascripts/environments/components/delete_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
import deleteEnvironmentMutation from '../graphql/mutations/delete_environment.mutation.graphql';
@@ -65,11 +65,11 @@ export default {
.then(({ data }) => {
const [message] = data?.deleteEvironment?.errors ?? [];
if (message) {
- createFlash({ message });
+ createAlert({ message });
}
})
.catch((error) =>
- createFlash({
+ createAlert({
message: s__(
'Environments|An error occurred while deleting the environment. Check if the environment stopped; if not, stop it and try again.',
),
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index 3475b38c8c9..b00a0777a03 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -10,7 +10,7 @@ import {
import { __, s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import deploymentDetails from '../graphql/queries/deployment_details.query.graphql';
import DeploymentStatusBadge from './deployment_status_badge.vue';
import Commit from './commit.vue';
@@ -119,7 +119,7 @@ export default {
return data?.project?.deployment?.tags;
},
error(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.LOAD_ERROR_MESSAGE,
captureError: true,
error,
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index 96742a11ebb..901d0f5b34d 100644
--- a/app/assets/javascripts/environments/components/edit_environment.vue
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -1,5 +1,5 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import EnvironmentForm from './environment_form.vue';
@@ -39,7 +39,7 @@ export default {
.then(({ data: { path } }) => visitUrl(path))
.catch((error) => {
const message = error.response.data.message[0];
- createFlash({ message });
+ createAlert({ message });
this.loading = false;
});
},
diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue
index 563fa6c96fb..e40c37b5095 100644
--- a/app/assets/javascripts/environments/components/empty_state.vue
+++ b/app/assets/javascripts/environments/components/empty_state.vue
@@ -1,9 +1,14 @@
<script>
+import { GlEmptyState, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import { ENVIRONMENTS_SCOPE } from '../constants';
export default {
- name: 'EnvironmentsEmptyState',
+ components: {
+ GlEmptyState,
+ GlLink,
+ },
+ inject: ['newEnvironmentPath'],
props: {
helpPath: {
type: String,
@@ -13,10 +18,23 @@ export default {
type: String,
required: true,
},
+ hasTerm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
title() {
- return this.$options.i18n.title[this.scope];
+ return this.hasTerm
+ ? this.$options.i18n.searchingTitle
+ : this.$options.i18n.title[this.scope];
+ },
+ content() {
+ return this.hasTerm ? this.$options.i18n.searchingContent : this.$options.i18n.content;
+ },
+ buttonText() {
+ return this.hasTerm ? this.$options.i18n.newEnvironmentButtonLabel : '';
},
},
i18n: {
@@ -27,20 +45,21 @@ export default {
content: s__(
'Environments|Environments are places where code gets deployed, such as staging or production.',
),
+ searchingTitle: s__('Environments|No results found'),
+ searchingContent: s__('Environments|Edit your search and try again'),
link: s__('Environments|How do I create an environment?'),
+ newEnvironmentButtonLabel: s__('Environments|New environment'),
},
};
</script>
<template>
- <div class="empty-state">
- <div class="text-content">
- <h4 class="js-blank-state-title">
- {{ title }}
- </h4>
- <p>
- {{ $options.i18n.content }}
- <a :href="helpPath"> {{ $options.i18n.link }} </a>
- </p>
- </div>
- </div>
+ <gl-empty-state :primary-button-text="buttonText" :primary-button-link="newEnvironmentPath">
+ <template #title>
+ <h4>{{ title }}</h4>
+ </template>
+ <template #description>
+ <p>{{ content }}</p>
+ <gl-link v-if="!hasTerm" :href="helpPath">{{ $options.i18n.link }}</gl-link>
+ </template>
+ </gl-empty-state>
</template>
diff --git a/app/assets/javascripts/environments/components/enable_review_app_modal.vue b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
index 6343fe8702a..420ad3d9c42 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_modal.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_modal.vue
@@ -1,18 +1,19 @@
<script>
-import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlIcon, GlPopover } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { REVIEW_APP_MODAL_I18N as i18n } from '../constants';
export default {
components: {
GlLink,
GlModal,
GlSprintf,
+ GlIcon,
+ GlPopover,
ModalCopyButton,
},
- inject: ['defaultBranchName'],
model: {
prop: 'visible',
event: 'change',
@@ -28,25 +29,6 @@ export default {
default: false,
},
},
- instructionText: {
- step1: s__(
- 'EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}.',
- ),
- step2: s__('EnableReviewApp|%{stepStart}Step 2%{stepEnd}. Copy the following snippet:'),
- step3: s__(
- `EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file.`,
- ),
- step4: s__(
- `EnableReviewApp|%{stepStart}Step 4 (optional)%{stepEnd}. Enable Visual Reviews by following the %{linkStart}setup instructions%{linkEnd}.`,
- ),
- },
- modalInfo: {
- closeText: s__('EnableReviewApp|Close'),
- copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
- title: s__('ReviewApp|Enable Review App'),
- },
- visualReviewsDocs: helpPagePath('ci/review_apps/index.md', { anchor: 'visual-reviews' }),
- connectClusterDocs: helpPagePath('user/clusters/agent/index'),
data() {
const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
@@ -57,81 +39,99 @@ export default {
return `deploy_review:
stage: deploy
script:
- - echo "Deploy a review app"
+ - echo "Add script here that deploys the code to your infrastructure"
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
- only:
- - branches
- except:
- - ${this.defaultBranchName}`;
+ rules:
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"`;
+ },
+ },
+ methods: {
+ commaOrPeriod(index, length) {
+ return index + 1 === length ? '.' : ',';
},
},
+ i18n,
+ configuringReviewAppsPath: helpPagePath('ci/review_apps/index.md', {
+ anchor: 'configuring-review-apps',
+ }),
+ reviewAppsExamplesPath: helpPagePath('ci/review_apps/index.md', {
+ anchor: 'review-apps-examples',
+ }),
};
</script>
<template>
<gl-modal
:visible="visible"
:modal-id="modalId"
- :title="$options.modalInfo.title"
+ :title="$options.i18n.title"
static
size="lg"
- ok-only
- ok-variant="light"
- :ok-title="$options.modalInfo.closeText"
+ hide-footer
@change="$emit('change', $event)"
>
+ <p>{{ $options.i18n.intro }}</p>
<p>
- <gl-sprintf :message="$options.instructionText.step1">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link :href="$options.connectClusterDocs" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <strong>{{ $options.i18n.instructions.title }}</strong>
</p>
- <div>
- <p>
- <gl-sprintf :message="$options.instructionText.step2">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- </gl-sprintf>
- </p>
- <div class="gl-display-flex align-items-start">
- <pre :id="modalInfoCopyId" class="gl-w-full" data-testid="enable-review-app-copy-string">
- {{ modalInfoCopyStr }} </pre
- >
- <modal-copy-button
- :title="$options.modalInfo.copyToClipboardText"
- :modal-id="modalId"
- css-classes="border-0"
- :target="`#${modalInfoCopyId}`"
- />
- </div>
+ <div class="gl-mb-6">
+ <ol class="gl-px-6">
+ <li>
+ {{ $options.i18n.instructions.step1 }}
+ <gl-icon
+ ref="informationIcon"
+ name="information-o"
+ class="gl-text-blue-600 gl-hover-cursor-pointer"
+ />
+ <gl-popover
+ :target="() => $refs.informationIcon.$el"
+ :title="$options.i18n.staticSitePopover.title"
+ triggers="hover focus"
+ >
+ {{ $options.i18n.staticSitePopover.body }}
+ </gl-popover>
+ </li>
+ <li>{{ $options.i18n.instructions.step2 }}</li>
+ <li>
+ {{ $options.i18n.instructions.step3 }}
+ <ul class="gl-px-4 gl-py-2">
+ <li>{{ $options.i18n.instructions.step3a }}</li>
+ <li>
+ <gl-sprintf :message="$options.i18n.instructions.step3b">
+ <template #code="{ content }"
+ ><code>{{ content }}</code></template
+ >
+ </gl-sprintf>
+ </li>
+ <li class="gl-list-style-none">
+ <div class="gl-display-flex align-items-start">
+ <pre
+ :id="modalInfoCopyId"
+ class="gl-w-full"
+ data-testid="enable-review-app-copy-string"
+ >{{ modalInfoCopyStr }}</pre
+ >
+ <modal-copy-button
+ :title="$options.i18n.copyToClipboardText"
+ :modal-id="modalId"
+ css-classes="border-0"
+ :target="`#${modalInfoCopyId}`"
+ />
+ </div>
+ </li>
+ </ul>
+ </li>
+ <li>{{ $options.i18n.instructions.step4 }}</li>
+ </ol>
+ <gl-link :href="$options.configuringReviewAppsPath" target="_blank">
+ {{ $options.i18n.learnMore }}
+ <gl-icon name="external-link" />
+ </gl-link>
+ <gl-link :href="$options.reviewAppsExamplesPath" target="_blank" class="gl-ml-6">
+ {{ $options.i18n.viewMoreExampleProjects }}
+ <gl-icon name="external-link" />
+ </gl-link>
</div>
- <p>
- <gl-sprintf :message="$options.instructionText.step3">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link :href="`blob/${defaultBranchName}/.gitlab-ci.yml`" target="_blank">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p>
- <gl-sprintf :message="$options.instructionText.step4">
- <template #step="{ content }">
- <strong>{{ content }}</strong>
- </template>
- <template #link="{ content }">
- <gl-link :href="$options.visualReviewsDocs" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue
index b8def676e7d..04a390fbba7 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.vue
+++ b/app/assets/javascripts/environments/components/environment_external_url.vue
@@ -1,6 +1,8 @@
<script>
import { GlTooltipDirective, GlButton } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { s__, __ } from '~/locale';
+import { isSafeURL } from '~/lib/utils/url_utility';
/**
* Renders the external url link in environments table.
@@ -8,6 +10,7 @@ import { s__ } from '~/locale';
export default {
components: {
GlButton,
+ ModalCopyButton,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -21,11 +24,19 @@ export default {
i18n: {
title: s__('Environments|Open live environment'),
open: s__('Environments|Open'),
+ copy: __('Copy URL'),
+ copyTitle: s__('Environments|Copy live environment URL'),
+ },
+ computed: {
+ isSafeUrl() {
+ return isSafeURL(this.externalUrl);
+ },
},
};
</script>
<template>
<gl-button
+ v-if="isSafeUrl"
v-gl-tooltip
:title="$options.i18n.title"
:aria-label="$options.i18n.title"
@@ -37,4 +48,7 @@ export default {
>
{{ $options.i18n.open }}
</gl-button>
+ <modal-copy-button v-else :title="$options.i18n.copyTitle" :text="externalUrl">
+ {{ $options.i18n.copy }}
+ </modal-copy-button>
</template>
diff --git a/app/assets/javascripts/environments/components/environment_folder.vue b/app/assets/javascripts/environments/components/environment_folder.vue
index 881f404340d..2f6c54e4707 100644
--- a/app/assets/javascripts/environments/components/environment_folder.vue
+++ b/app/assets/javascripts/environments/components/environment_folder.vue
@@ -24,6 +24,10 @@ export default {
type: String,
required: true,
},
+ search: {
+ type: String,
+ required: true,
+ },
},
data() {
return { visible: false, interval: undefined };
@@ -32,7 +36,11 @@ export default {
folder: {
query: folderQuery,
variables() {
- return { environment: this.nestedEnvironment.latest, scope: this.scope };
+ return {
+ environment: this.nestedEnvironment.latest,
+ scope: this.scope,
+ search: this.search,
+ };
},
},
interval: {
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index f44182e822b..55e6a891e27 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,7 +1,9 @@
<script>
-import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import { GlBadge, GlPagination, GlSearchBoxByType, GlTab, GlTabs } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
@@ -31,6 +33,7 @@ export default {
StopEnvironmentModal,
GlBadge,
GlPagination,
+ GlSearchBoxByType,
GlTab,
GlTabs,
},
@@ -41,11 +44,10 @@ export default {
return {
scope: this.scope,
page: this.page ?? 1,
+ search: this.search,
};
},
- pollInterval() {
- return this.interval;
- },
+ pollInterval: 3000,
},
interval: {
query: pollIntervalQuery,
@@ -80,10 +82,11 @@ export default {
next: __('Next'),
prev: __('Prev'),
goto: (page) => sprintf(__('Go to page %{page}'), { page }),
+ searchPlaceholder: s__('Environments|Search by environment name'),
},
modalId: 'enable-review-app-info',
data() {
- const { page = '1', scope } = queryToObject(window.location.search);
+ const { page = '1', search = '', scope } = queryToObject(window.location.search);
return {
interval: undefined,
isReviewAppModalVisible: false,
@@ -97,6 +100,7 @@ export default {
environmentToStop: {},
environmentToChangeCanary: {},
weight: 0,
+ search,
};
},
computed: {
@@ -112,6 +116,9 @@ export default {
hasEnvironments() {
return this.environments.length > 0 || this.folders.length > 0;
},
+ hasSearch() {
+ return Boolean(this.search);
+ },
availableCount() {
return this.environmentApp?.availableCount;
},
@@ -152,11 +159,19 @@ export default {
return this.pageInfo?.perPage;
},
},
+ watch: {
+ interval(val) {
+ this.$apollo.queries.environmentApp.stopPolling();
+ this.$apollo.queries.environmentApp.startPolling(val);
+ },
+ },
mounted() {
window.addEventListener('popstate', this.syncPageFromQueryParams);
+ window.addEventListener('popstate', this.syncSearchFromQueryParams);
},
destroyed() {
window.removeEventListener('popstate', this.syncPageFromQueryParams);
+ window.removeEventListener('popstate', this.syncSearchFromQueryParams);
this.$apollo.queries.environmentApp.stopPolling();
},
methods: {
@@ -173,23 +188,24 @@ export default {
moveToPage(page) {
this.page = page;
updateHistory({
- url: setUrlParams({ page: this.page }),
+ url: setUrlParams({ page: this.page, scope: this.scope, search: this.search }),
title: document.title,
});
- this.resetPolling();
},
+ setSearch: debounce(function setSearch(input) {
+ this.search = input;
+ this.moveToPage(1);
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
syncPageFromQueryParams() {
const { page = '1' } = queryToObject(window.location.search);
this.page = parseInt(page, 10);
},
- resetPolling() {
- this.$apollo.queries.environmentApp.stopPolling();
+ syncSearchFromQueryParams() {
+ const { search = '' } = queryToObject(window.location.search);
+ this.search = search;
+ },
+ refetchEnvironments() {
this.$apollo.queries.environmentApp.refetch();
- this.$nextTick(() => {
- if (this.interval) {
- this.$apollo.queries.environmentApp.startPolling(this.interval);
- }
- });
},
},
ENVIRONMENTS_SCOPE,
@@ -237,12 +253,19 @@ export default {
</template>
</gl-tab>
</gl-tabs>
+ <gl-search-box-by-type
+ class="gl-mb-4"
+ :value="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ @input="setSearch"
+ />
<template v-if="hasEnvironments">
<environment-folder
v-for="folder in folders"
:key="folder.name"
class="gl-mb-3"
:scope="scope"
+ :search="search"
:nested-environment="folder"
/>
<environment-item
@@ -250,10 +273,15 @@ export default {
:key="environment.name"
class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
:environment="environment.latest"
- @change="resetPolling"
+ @change="refetchEnvironments"
/>
</template>
- <empty-state v-else :help-path="helpPagePath" :scope="scope" />
+ <empty-state
+ v-else-if="!$apollo.queries.environmentApp.loading"
+ :help-path="helpPagePath"
+ :scope="scope"
+ :has-term="hasSearch"
+ />
<gl-pagination
align="center"
:total-items="totalItems"
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index bd67908a6b4..bb2f053b3fc 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -4,6 +4,8 @@ import csrf from '~/lib/utils/csrf';
import { __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { isSafeURL } from '~/lib/utils/url_utility';
import DeleteEnvironmentModal from './delete_environment_modal.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
@@ -16,6 +18,7 @@ export default {
TimeAgo,
DeleteEnvironmentModal,
StopEnvironmentModal,
+ ModalCopyButton,
},
directives: {
GlModalDirective,
@@ -73,6 +76,8 @@ export default {
deleteButtonText: s__('Environments|Delete'),
externalButtonTitle: s__('Environments|Open live environment'),
externalButtonText: __('View deployment'),
+ copyUrlText: __('Copy URL'),
+ copyUrlTitle: s__('Environments|Copy live environment URL'),
cancelAutoStopButtonTitle: __('Prevent environment from auto-stopping'),
},
computed: {
@@ -82,6 +87,9 @@ export default {
shouldShowExternalUrlButton() {
return Boolean(this.environment.externalUrl);
},
+ isSafeUrl() {
+ return isSafeURL(this.environment.externalUrl);
+ },
shouldShowStopButton() {
return this.canStopEnvironment && this.environment.isAvailable;
},
@@ -123,16 +131,25 @@ export default {
:href="terminalPath"
icon="terminal"
/>
- <gl-button
- v-if="shouldShowExternalUrlButton"
- v-gl-tooltip.hover
- data-testid="external-url-button"
- :title="$options.i18n.externalButtonTitle"
- :href="environment.externalUrl"
- icon="external-link"
- target="_blank"
- >{{ $options.i18n.externalButtonText }}</gl-button
- >
+ <template v-if="shouldShowExternalUrlButton">
+ <gl-button
+ v-if="isSafeUrl"
+ v-gl-tooltip.hover
+ data-testid="external-url-button"
+ :title="$options.i18n.externalButtonTitle"
+ :href="environment.externalUrl"
+ icon="external-link"
+ target="_blank"
+ >{{ $options.i18n.externalButtonText }}</gl-button
+ >
+ <modal-copy-button
+ v-else
+ :title="$options.i18n.copyUrlTitle"
+ :text="environment.externalUrl"
+ >
+ {{ $options.i18n.copyUrlText }}
+ </modal-copy-button>
+ </template>
<gl-button
v-if="shouldShowExternalUrlButton"
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/environments/components/new_environment.vue b/app/assets/javascripts/environments/components/new_environment.vue
index 14da2668417..bb4d6ab3428 100644
--- a/app/assets/javascripts/environments/components/new_environment.vue
+++ b/app/assets/javascripts/environments/components/new_environment.vue
@@ -1,5 +1,5 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import EnvironmentForm from './environment_form.vue';
@@ -32,7 +32,7 @@ export default {
.then(({ data: { path } }) => visitUrl(path))
.catch((error) => {
const message = error.response.data.message[0];
- createFlash({ message });
+ createAlert({ message });
this.loading = false;
});
},
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 942491039d6..c4d02da9d21 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
// These statuses are based on how the backend defines pod phases here
// lib/gitlab/kubernetes/pod.rb
@@ -48,3 +48,32 @@ export const ENVIRONMENT_COUNT_BY_SCOPE = {
[ENVIRONMENTS_SCOPE.AVAILABLE]: 'availableCount',
[ENVIRONMENTS_SCOPE.STOPPED]: 'stoppedCount',
};
+
+export const REVIEW_APP_MODAL_I18N = {
+ title: s__('ReviewApp|Enable Review App'),
+ intro: s__(
+ 'EnableReviewApp|Review apps are dynamic environments that you can use to provide a live preview of changes made in a feature branch.',
+ ),
+ instructions: {
+ title: s__('EnableReviewApp|To configure a dynamic review app, you must:'),
+ step1: s__(
+ 'EnableReviewApp|Have access to infrastructure that can host and deploy the review apps.',
+ ),
+ step2: s__('EnableReviewApp|Install and configure a runner to do the deployment.'),
+ step3: s__('EnableReviewApp|Add a job in your CI/CD configuration that:'),
+ step3a: s__('EnableReviewApp|Only runs for feature branches or merge requests.'),
+ step3b: s__(
+ 'EnableReviewApp|Uses a predefined CI/CD variable like %{codeStart}$(CI_COMMIT_REF_SLUG)%{codeEnd} to dynamically create the review app environments. For example, for a configuration using merge request pipelines:',
+ ),
+ step4: s__('EnableReviewApp|Recommended: Set up a job that manually stops the Review Apps.'),
+ },
+ staticSitePopover: {
+ title: s__('EnableReviewApp|Using a static site?'),
+ body: s__(
+ 'EnableReviewApp|Make sure your project has an environment configured with the target URL set to your website URL. If not, create a new one before continuing.',
+ ),
+ },
+ learnMore: __('Learn more'),
+ viewMoreExampleProjects: s__('EnableReviewApp|View more example projects'),
+ copyToClipboardText: s__('EnableReviewApp|Copy snippet'),
+};
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
index c3ab9cf7fca..1a572208a1c 100644
--- a/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/environment_app.query.graphql
@@ -1,5 +1,5 @@
-query getEnvironmentApp($page: Int, $scope: String) {
- environmentApp(page: $page, scope: $scope) @client {
+query getEnvironmentApp($page: Int, $scope: String, $search: String) {
+ environmentApp(page: $page, scope: $scope, search: $search) @client {
availableCount
stoppedCount
environments
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index e8c145ee916..c662acb8f93 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,5 +1,5 @@
-query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String) {
- folder(environment: $environment, scope: $scope) @client {
+query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
+ folder(environment: $environment, scope: $scope, search: $search) @client {
availableCount
environments
stoppedCount
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 722bb78bcf9..afd56d0cf0d 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -30,8 +30,8 @@ const mapEnvironment = (env) => ({
export const resolvers = (endpoint) => ({
Query: {
- environmentApp(_context, { page, scope }, { cache }) {
- return axios.get(endpoint, { params: { nested: true, page, scope } }).then((res) => {
+ environmentApp(_context, { page, scope, search }, { cache }) {
+ return axios.get(endpoint, { params: { nested: true, page, scope, search } }).then((res) => {
const headers = normalizeHeaders(res.headers);
const interval = headers['POLL-INTERVAL'];
const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' };
@@ -59,8 +59,8 @@ export const resolvers = (endpoint) => ({
};
});
},
- folder(_, { environment: { folderPath }, scope }) {
- return axios.get(folderPath, { params: { scope, per_page: 3 } }).then((res) => ({
+ folder(_, { environment: { folderPath }, scope, search }) {
+ return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
availableCount: res.data.available_count,
environments: res.data.environments.map(mapEnvironment),
stoppedCount: res.data.stopped_count,
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index 8957a3074ed..5e936ad8c96 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -3,7 +3,7 @@
*/
import { isEqual, isFunction, omitBy } from 'lodash';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { getParameterByName } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
@@ -94,7 +94,7 @@ export default {
errorCallback() {
this.isLoading = false;
- createFlash({
+ createAlert({
message: s__('Environments|An error occurred while fetching the environments.'),
});
},
@@ -123,7 +123,7 @@ export default {
})
.catch((err) => {
this.isLoading = false;
- createFlash({
+ createAlert({
message: isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage,
});
});
@@ -179,7 +179,7 @@ export default {
window.location.href = url.join('/');
})
.catch(() => {
- createFlash({
+ createAlert({
message: errorMessage,
});
});
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index a602c92a840..b02c3cd2cba 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -13,7 +13,7 @@ import {
GlIcon,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
import { __, sprintf, n__ } from '~/locale';
import Tracking from '~/tracking';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -89,7 +89,7 @@ export default {
pollInterval: 2000,
update: (data) => data.project.sentryErrors.detailedError,
error: () =>
- createFlash({
+ createAlert({
message: __('Failed to load error details from Sentry.'),
}),
result(res) {
@@ -234,9 +234,9 @@ export default {
if (Date.now() > this.errorPollTimeout) {
this.$apollo.queries.error.stopPolling();
this.errorLoading = false;
- createFlash({
+ createAlert({
message: __('Could not connect to Sentry. Refresh the page to try again.'),
- type: 'warning',
+ variant: VARIANT_WARNING,
});
}
},
diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js
index fbfcd6ce2df..603f8611005 100644
--- a/app/assets/javascripts/error_tracking/store/actions.js
+++ b/app/assets/javascripts/error_tracking/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import service from '../services';
@@ -18,7 +18,7 @@ export const updateStatus = ({ commit }, { endpoint, redirectUrl, status }) =>
return resp.data.result;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to update issue status'),
}),
);
diff --git a/app/assets/javascripts/error_tracking/store/details/actions.js b/app/assets/javascripts/error_tracking/store/details/actions.js
index 09fa650f64b..1409399940a 100644
--- a/app/assets/javascripts/error_tracking/store/details/actions.js
+++ b/app/assets/javascripts/error_tracking/store/details/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import service from '../../services';
@@ -26,7 +26,7 @@ export function startPollingStacktrace({ commit }, endpoint) {
},
errorCallback: () => {
commit(types.SET_LOADING_STACKTRACE, false);
- createFlash({
+ createAlert({
message: __('Failed to load stacktrace.'),
});
},
diff --git a/app/assets/javascripts/error_tracking/store/list/actions.js b/app/assets/javascripts/error_tracking/store/list/actions.js
index 418056314f6..f633711add3 100644
--- a/app/assets/javascripts/error_tracking/store/list/actions.js
+++ b/app/assets/javascripts/error_tracking/store/list/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __ } from '~/locale';
import Service from '../../services';
@@ -33,7 +33,7 @@ export function startPolling({ state, commit, dispatch }) {
},
errorCallback: () => {
commit(types.SET_LOADING, false);
- createFlash({
+ createAlert({
message: __('Failed to load errors from Sentry.'),
});
},
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index 70fb1fa9cd7..3bc91a2adbf 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -190,7 +190,7 @@ export default {
<gl-form-radio name="error-tracking-integrated" :value="true">
{{ __('GitLab') }}
<template #help>
- {{ __('Uses GitLab as a lightweight alternative to Sentry.') }}
+ {{ __('Uses GitLab as an alternative to Sentry.') }}
</template>
</gl-form-radio>
</gl-form-radio-group>
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index 972ad58c617..4d6fe767f3a 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -46,7 +46,7 @@ export const requestSettings = ({ commit }) => {
export const receiveSettingsError = ({ commit }, { response = {} }) => {
const message = response.data && response.data.message ? response.data.message : '';
- createFlash({
+ createAlert({
message: `${__('There was an error saving your changes.')} ${message}`,
});
commit(types.UPDATE_SETTINGS_LOADING, false);
diff --git a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
index 70b60b4b113..ce5f7915dbf 100644
--- a/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/environments_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlSearchBoxByType } from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -87,7 +87,7 @@ export default {
.catch(() => {
this.isLoading = false;
this.closeSuggestions();
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again.'),
});
});
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index 98982920121..89400bc4742 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -8,7 +8,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
@@ -52,7 +52,7 @@ export default {
this.results = data || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again.'),
});
})
diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js
index 8656479190a..97c22781ac5 100644
--- a/app/assets/javascripts/feature_flags/store/edit/actions.js
+++ b/app/assets/javascripts/feature_flags/store/edit/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -49,7 +49,7 @@ export const receiveFeatureFlagSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAG_SUCCESS, response);
export const receiveFeatureFlagError = ({ commit }) => {
commit(types.RECEIVE_FEATURE_FLAG_ERROR);
- createFlash({
+ createAlert({
message: __('Something went wrong on our end. Please try again!'),
});
};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index b26a96499ba..a9542a9667e 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -10,7 +10,7 @@ export function dismiss(endpoint, highlightId) {
feature_name: highlightId,
})
.catch(() =>
- createFlash({
+ createAlert({
message: __(
'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
),
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 9726b2164b7..23591fc0667 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import AjaxFilter from './droplab/plugins/ajax_filter';
import DropdownUtils from './dropdown_utils';
@@ -27,7 +27,7 @@ export default class DropdownAjaxFilter extends FilteredSearchDropdown {
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
onError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the dropdown data.'),
});
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index aeea66bf51c..8c50c1860ec 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import Ajax from './droplab/plugins/ajax';
import Filter from './droplab/plugins/filter';
@@ -14,7 +14,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown {
method: 'setData',
loadingTemplate: this.loadingTemplate,
onError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the dropdown data.'),
});
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index ddc3c06a9d1..ab95986dc62 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import Ajax from './droplab/plugins/ajax';
import Filter from './droplab/plugins/filter';
@@ -17,7 +17,7 @@ export default class DropdownNonUser extends FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
preprocessing,
onError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the dropdown data.'),
});
},
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index ac2cf27e873..bc0f5398b4c 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,7 +1,7 @@
import { last } from 'lodash';
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
ENTER_KEY_CODE,
BACKSPACE_KEY_CODE,
@@ -91,7 +91,7 @@ export default class FilteredSearchManager {
.fetch()
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
- createFlash({
+ createAlert({
message: __('An error occurred while parsing recent searches'),
});
// Gracefully fail to empty array
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 0d144398531..1ad2006d689 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -4,7 +4,7 @@ import * as Emoji from '~/emoji';
import FilteredSearchContainer from '~/filtered_search/container';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
@@ -85,7 +85,7 @@ export default class VisualTokenValue {
);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching label colors.'),
}),
);
@@ -111,7 +111,7 @@ export default class VisualTokenValue {
VisualTokenValue.replaceEpicTitle(tokenValueContainer, matchingEpic.title, matchingEpic.id);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while adding formatted title for epic'),
}),
);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index edf83a33812..5665231e613 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -158,7 +158,7 @@ const createAlert = function createAlert({
onDismiss();
}
this.$destroy();
- this.$el.parentNode.removeChild(this.$el);
+ this.$el.parentNode?.removeChild(this.$el);
},
},
render(h) {
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index d376c9f76ba..0a4733de65f 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -19,7 +19,7 @@ export default class GpgBadges {
badges.children().attr('aria-label', __('Loading'));
const displayError = () =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading commit signatures'),
});
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
index 25347ad6433..db2fd3cc256 100644
--- a/app/assets/javascripts/grafana_integration/store/actions.js
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -38,7 +38,7 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => {
const { response } = error;
const message = response.data && response.data.message ? response.data.message : '';
- createFlash({
+ createAlert({
message: `${__('There was an error saving your changes.')} ${message}`,
});
};
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index e86103c332b..3b737dfff33 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -4,14 +4,13 @@ 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 workItemQuery from '~/work_items/graphql/work_item.query.graphql';
-import { WIDGET_TYPE_LABELS } from '~/work_items/constants';
+import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants';
export const temporaryConfig = {
typeDefs,
cacheConfig: {
possibleTypes: {
- LocalWorkItemWidget: ['LocalWorkItemLabels'],
+ LocalWorkItemWidget: ['LocalWorkItemMilestone'],
},
typePolicies: {
Project: {
@@ -28,18 +27,32 @@ export const temporaryConfig = {
return (
widgets || [
{
- __typename: 'LocalWorkItemLabels',
- type: WIDGET_TYPE_LABELS,
- allowScopedLabels: true,
- nodes: [],
+ __typename: 'LocalWorkItemMilestone',
+ type: WIDGET_TYPE_MILESTONE,
+ nodes: [
+ {
+ dueDate: null,
+ expired: false,
+ id: 'gid://gitlab/Milestone/30',
+ title: 'v4.0',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Milestone',
+ },
+ ],
},
]
);
},
},
widgets: {
- merge(_, incoming) {
- return incoming;
+ merge(existing = [], incoming) {
+ if (existing.length === 0) {
+ return incoming;
+ }
+ return existing.map((existingWidget) => {
+ const incomingWidget = incoming.find((w) => w.type === existingWidget.type);
+ return incomingWidget || existingWidget;
+ });
},
},
},
@@ -62,27 +75,6 @@ export const resolvers = {
});
cache.writeQuery({ query: getIssueStateQuery, data });
},
- localUpdateWorkItem(_, { input }, { cache }) {
- const sourceData = cache.readQuery({
- query: workItemQuery,
- variables: { id: input.id },
- });
-
- const data = produce(sourceData, (draftData) => {
- if (input.labels) {
- const labelsWidget = draftData.workItem.mockWidgets.find(
- (widget) => widget.type === WIDGET_TYPE_LABELS,
- );
- labelsWidget.nodes = [...input.labels];
- }
- });
-
- cache.writeQuery({
- query: workItemQuery,
- variables: { id: input.id },
- data,
- });
- },
},
};
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index c6bd9e563c0..545c150e536 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -144,7 +144,7 @@
"WorkItemWidgetIteration",
"WorkItemWidgetLabels",
"WorkItemWidgetStartAndDueDate",
- "WorkItemWidgetVerificationStatus",
+ "WorkItemWidgetStatus",
"WorkItemWidgetWeight"
]
}
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
index f64c4276deb..c05b4a5950c 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search.query.graphql
@@ -9,6 +9,7 @@ query projectUsersSearch($search: String!, $fullPath: ID!, $after: String, $firs
relations: [DIRECT, INHERITED, INVITED_GROUPS]
first: $first
after: $after
+ sort: USER_FULL_NAME_ASC
) {
pageInfo {
hasNextPage
diff --git a/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql b/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
index 2bd016feb19..5a589b094de 100644
--- a/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/users_search_with_mr_permissions.graphql
@@ -8,7 +8,11 @@ query projectUsersSearchWithMRPermissions(
) {
workspace: project(fullPath: $fullPath) {
id
- users: projectMembers(search: $search, relations: [DIRECT, INHERITED, INVITED_GROUPS]) {
+ users: projectMembers(
+ search: $search
+ relations: [DIRECT, INHERITED, INVITED_GROUPS]
+ sort: USER_FULL_NAME_ASC
+ ) {
nodes {
id
mergeRequestInteraction(id: $mergeRequestId) {
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index 49e7dd28ff6..cc70d832edc 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
@@ -77,7 +77,7 @@ export default class Group {
element.value = suggestedSlug;
});
} else if (exists && !suggests.length) {
- createFlash({
+ createAlert({
message: __('Unable to suggest a path. Please refresh and try again.'),
});
}
@@ -87,7 +87,7 @@ export default class Group {
return;
}
- createFlash({
+ createAlert({
message: __('An error occurred while checking group path. Please refresh and try again.'),
});
});
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 0bd7371d39b..15f5a3518a5 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
@@ -51,7 +51,6 @@ export default {
isModalVisible: false,
isLoading: true,
isSearchEmpty: false,
- searchEmptyMessage: '',
targetGroup: null,
targetParentGroup: null,
showEmptyState: false,
@@ -88,15 +87,12 @@ export default {
},
},
created() {
- this.searchEmptyMessage = this.hideProjects
- ? COMMON_STR.GROUP_SEARCH_EMPTY
- : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY;
-
eventHub.$on(`${this.action}fetchPage`, this.fetchPage);
eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren);
eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$on(`${this.action}updatePagination`, this.updatePagination);
eventHub.$on(`${this.action}updateGroups`, this.updateGroups);
+ eventHub.$on(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
},
mounted() {
this.fetchAllGroups();
@@ -111,6 +107,7 @@ export default {
eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal);
eventHub.$off(`${this.action}updatePagination`, this.updatePagination);
eventHub.$off(`${this.action}updateGroups`, this.updateGroups);
+ eventHub.$off(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups);
},
methods: {
hideModal() {
@@ -132,7 +129,7 @@ export default {
this.isLoading = false;
window.scrollTo({ top: 0, behavior: 'smooth' });
- createFlash({ message: COMMON_STR.FAILURE });
+ createAlert({ message: COMMON_STR.FAILURE });
});
},
fetchAllGroups() {
@@ -153,6 +150,18 @@ export default {
this.updateGroups(res, Boolean(this.filterGroupsBy));
});
},
+ fetchFilteredAndSortedGroups({ filterGroupsBy, sortBy }) {
+ this.isLoading = true;
+
+ return this.fetchGroups({
+ filterGroupsBy,
+ sortBy,
+ updatePagination: true,
+ }).then((res) => {
+ this.isLoading = false;
+ this.updateGroups(res, Boolean(filterGroupsBy));
+ });
+ },
fetchPage({ page, filterGroupsBy, sortBy, archived }) {
this.isLoading = true;
@@ -218,7 +227,7 @@ export default {
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
}
- createFlash({ message });
+ createAlert({ message });
this.targetGroup.isBeingRemoved = false;
});
},
@@ -245,7 +254,7 @@ export default {
const hasGroups = groups && groups.length > 0;
if (this.renderEmptyState) {
- this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups;
+ this.isSearchEmpty = fromSearch && !hasGroups;
} else {
this.isSearchEmpty = !hasGroups;
}
@@ -280,7 +289,6 @@ export default {
v-else
:groups="groups"
:search-empty="isSearchEmpty"
- :search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 3a05c308a2a..43aa0753082 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,11 +1,18 @@
<script>
+import { GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { getParameterByName } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
import eventHub from '../event_hub';
export default {
+ i18n: {
+ emptyStateTitle: __('No results found'),
+ emptyStateDescription: __('Edit your search and try again'),
+ },
components: {
PaginationLinks,
+ GlEmptyState,
},
props: {
groups: {
@@ -20,10 +27,6 @@ export default {
type: Boolean,
required: true,
},
- searchEmptyMessage: {
- type: String,
- required: true,
- },
action: {
type: String,
required: false,
@@ -43,12 +46,11 @@ export default {
<template>
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
- <div
+ <gl-empty-state
v-if="searchEmpty"
- class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5"
- >
- {{ searchEmptyMessage }}
- </div>
+ :title="$options.i18n.emptyStateTitle"
+ :description="$options.i18n.emptyStateDescription"
+ />
<template v-else>
<group-folder :groups="groups" :action="action" />
<pagination-links
diff --git a/app/assets/javascripts/groups/components/new_top_level_group_alert.vue b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue
new file mode 100644
index 00000000000..c6af6cdb59f
--- /dev/null
+++ b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+export default {
+ name: 'NewTopLevelGroupAlert',
+ components: {
+ GlAlert,
+ UserCalloutDismisser,
+ },
+ i18n: {
+ titleText: s__("Groups|You're creating a new top-level group"),
+ bodyText: s__(
+ 'Groups|Members, projects, trials, and paid subscriptions are tied to a specific top-level group. If you are already a member of a top-level group, you can create a subgroup so your new work is part of your existing top-level group. Do you want to create a subgroup instead?',
+ ),
+ primaryBtnText: s__('Groups|Learn more about subgroups'),
+ },
+ subgroupsDocsPath: helpPagePath('user/group/subgroups/index'),
+};
+</script>
+
+<template>
+ <user-callout-dismisser feature-name="new_top_level_group_alert">
+ <template #default="{ dismiss, shouldShowCallout }">
+ <gl-alert
+ v-if="shouldShowCallout"
+ ref="newTopLevelAlert"
+ data-testid="new-top-level-alert"
+ :title="$options.i18n.titleText"
+ :primary-button-text="$options.i18n.primaryBtnText"
+ :primary-button-link="$options.subgroupsDocsPath"
+ @dismiss="dismiss"
+ >
+ {{ $options.i18n.bodyText }}
+ </gl-alert>
+ </template>
+ </user-callout-dismisser>
+</template>
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 325e42af0f8..d0c5846ac88 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -1,58 +1,77 @@
<script>
-import { GlTabs, GlTab } from '@gitlab/ui';
-import { isString } from 'lodash';
+import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { isString, debounce } from 'lodash';
import { __ } from '~/locale';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
import GroupsStore from '../store/groups_store';
import GroupsService from '../service/groups_service';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
+ OVERVIEW_TABS_SORTING_ITEMS,
} from '../constants';
+import eventHub from '../event_hub';
import GroupsApp from './app.vue';
+const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
+
export default {
- components: { GlTabs, GlTab, GroupsApp },
- inject: ['endpoints'],
+ components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
+ inject: ['endpoints', 'initialSort'],
data() {
+ const tabs = [
+ {
+ title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ renderEmptyState: true,
+ lazy: this.$route.name !== ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ store: new GroupsStore({ showSchemaMarkup: true }),
+ },
+ {
+ title: this.$options.i18n[ACTIVE_TAB_SHARED],
+ key: ACTIVE_TAB_SHARED,
+ renderEmptyState: false,
+ lazy: this.$route.name !== ACTIVE_TAB_SHARED,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
+ store: new GroupsStore(),
+ },
+ {
+ title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
+ key: ACTIVE_TAB_ARCHIVED,
+ renderEmptyState: false,
+ lazy: this.$route.name !== ACTIVE_TAB_ARCHIVED,
+ service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
+ store: new GroupsStore(),
+ },
+ ];
return {
- tabs: [
- {
- title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
- key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- renderEmptyState: true,
- lazy: false,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
- store: new GroupsStore({ showSchemaMarkup: true }),
- },
- {
- title: this.$options.i18n[ACTIVE_TAB_SHARED],
- key: ACTIVE_TAB_SHARED,
- renderEmptyState: false,
- lazy: true,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
- store: new GroupsStore(),
- },
- {
- title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
- key: ACTIVE_TAB_ARCHIVED,
- renderEmptyState: false,
- lazy: true,
- service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
- store: new GroupsStore(),
- },
- ],
- activeTabIndex: 0,
+ tabs,
+ activeTabIndex: tabs.findIndex((tab) => tab.key === this.$route.name),
+ sort: SORTING_ITEM_NAME,
+ isAscending: true,
+ search: '',
};
},
+ computed: {
+ activeTab() {
+ return this.tabs[this.activeTabIndex];
+ },
+ sortQueryStringValue() {
+ return this.isAscending ? this.sort.asc : this.sort.desc;
+ },
+ },
mounted() {
- const activeTabIndex = this.tabs.findIndex((tab) => tab.key === this.$route.name);
-
- if (activeTabIndex === -1) {
- return;
- }
+ this.search = this.$route.query?.filter || '';
- this.activeTabIndex = activeTabIndex;
+ const sortQueryStringValue = this.$route.query?.sort || this.initialSort;
+ const sort =
+ OVERVIEW_TABS_SORTING_ITEMS.find((sortOption) =>
+ [sortOption.asc, sortOption.desc].includes(sortQueryStringValue),
+ ) || SORTING_ITEM_NAME;
+ this.sort = sort;
+ this.isAscending = sort.asc === sortQueryStringValue;
},
methods: {
handleTabInput(tabIndex) {
@@ -72,14 +91,64 @@ export default {
? this.$route.params.group.split('/')
: this.$route.params.group;
- this.$router.push({ name: tab.key, params: { group: groupParam } });
+ this.$router.push({ name: tab.key, params: { group: groupParam }, query: this.$route.query });
+ },
+ handleSearchOrSortChange() {
+ // Update query string
+ const query = {};
+ if (this.sortQueryStringValue !== this.initialSort) {
+ query.sort = this.isAscending ? this.sort.asc : this.sort.desc;
+ }
+ if (this.search) {
+ query.filter = this.search;
+ }
+ this.$router.push({ query });
+
+ // Reset `lazy` prop so that groups/projects are fetched with updated `sort` and `filter` params when switching tabs
+ this.tabs.forEach((tab, index) => {
+ if (index === this.activeTabIndex) {
+ return;
+ }
+ // eslint-disable-next-line no-param-reassign
+ tab.lazy = true;
+ });
+
+ // Update data
+ eventHub.$emit(`${this.activeTab.key}fetchFilteredAndSortedGroups`, {
+ filterGroupsBy: this.search,
+ sortBy: this.sortQueryStringValue,
+ });
+ },
+ handleSortDirectionChange() {
+ this.isAscending = !this.isAscending;
+
+ this.handleSearchOrSortChange();
+ },
+ handleSortingItemClick(sortingItem) {
+ if (sortingItem === this.sort) {
+ return;
+ }
+
+ this.sort = sortingItem;
+
+ this.handleSearchOrSortChange();
+ },
+ handleSearchInput(value) {
+ this.search = value;
+
+ this.debouncedSearch();
},
+ debouncedSearch: debounce(async function debouncedSearch() {
+ this.handleSearchOrSortChange();
+ }, DEBOUNCE_DELAY),
},
i18n: {
[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'),
[ACTIVE_TAB_SHARED]: __('Shared projects'),
[ACTIVE_TAB_ARCHIVED]: __('Archived projects'),
+ searchPlaceholder: __('Search'),
},
+ OVERVIEW_TABS_SORTING_ITEMS,
};
</script>
@@ -99,5 +168,37 @@ export default {
:render-empty-state="renderEmptyState"
/>
</gl-tab>
+ <template #tabs-end>
+ <li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2">
+ <div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2">
+ <div class="gl-p-2 gl-lg-form-input-md gl-w-full">
+ <gl-search-box-by-type
+ :value="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ data-qa-selector="groups_filter_field"
+ @input="handleSearchInput"
+ />
+ </div>
+ <div class="gl-p-2 gl-w-full gl-lg-w-auto">
+ <gl-sorting
+ class="gl-w-full"
+ dropdown-class="gl-w-full"
+ data-testid="group_sort_by_dropdown"
+ :text="sort.label"
+ :is-ascending="isAscending"
+ @sortDirectionChange="handleSortDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="sortingItem in $options.OVERVIEW_TABS_SORTING_ITEMS"
+ :key="sortingItem.label"
+ :active="sortingItem === sort"
+ @click="handleSortingItemClick(sortingItem)"
+ >{{ sortingItem.label }}</gl-sorting-item
+ >
+ </gl-sorting>
+ </div>
+ </div>
+ </li>
+ </template>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 7e7282a27b0..e28459811d7 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -2,7 +2,7 @@
import { GlFormGroup } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
export const i18n = {
confirmationMessage: __(
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index 223c2975c11..6fb12cd6270 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -24,8 +24,6 @@ export const COMMON_STR = {
EDIT_BTN_TITLE: s__('GroupsTree|Edit'),
REMOVE_BTN_TITLE: s__('GroupsTree|Delete'),
OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'),
- GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'),
- GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'),
};
export const ITEM_TYPE = {
@@ -62,3 +60,26 @@ export const VISIBILITY_TYPE_ICON = {
[VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield',
[VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock',
};
+
+export const OVERVIEW_TABS_SORTING_ITEMS = [
+ {
+ label: __('Name'),
+ asc: 'name_asc',
+ desc: 'name_desc',
+ },
+ {
+ label: __('Created'),
+ asc: 'created_asc',
+ desc: 'created_desc',
+ },
+ {
+ label: __('Updated'),
+ asc: 'latest_activity_asc',
+ desc: 'latest_activity_desc',
+ },
+ {
+ label: __('Stars'),
+ asc: 'stars_asc',
+ desc: 'stars_desc',
+ },
+];
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index 4fa3682c729..664d07ca13d 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -51,6 +51,7 @@ export const initGroupOverviewTabs = () => {
subgroupsAndProjectsEndpoint,
sharedProjectsEndpoint,
archivedProjectsEndpoint,
+ initialSort,
} = el.dataset;
return new Vue({
@@ -70,6 +71,7 @@ export const initGroupOverviewTabs = () => {
[ACTIVE_TAB_SHARED]: sharedProjectsEndpoint,
[ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint,
},
+ initialSort,
},
render(createElement) {
return createElement(OverviewTabs);
diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
index 28f059fa23e..db8e424e166 100644
--- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getSubGroups } from '../api/access_dropdown_api';
import { LEVEL_TYPES } from '../constants';
@@ -98,7 +98,7 @@ export default {
this.consolidateData(groupsResponse.data);
this.setSelected({ initial });
})
- .catch(() => createFlash({ message: __('Failed to load groups.') }))
+ .catch(() => createAlert({ message: __('Failed to load groups.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 64bba91eb4d..34e984a9bb9 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,21 +1,11 @@
import $ from 'jquery';
import { escape } from 'lodash';
+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 groupsPath = (groupsFilter, parentGroupID) => {
- switch (groupsFilter) {
- case 'descendant_groups':
- return Api.descendantGroupsPath.replace(':id', parentGroupID);
- case 'subgroups':
- return Api.subgroupsPath.replace(':id', parentGroupID);
- default:
- return Api.groupsPath;
- }
-};
-
const groupsSelect = () => {
loadCSSFile(gon.select2_css_path)
.then(() => {
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index f4b939fb20f..8fc0ce48e61 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -14,6 +14,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { truncate } from '~/lib/utils/text_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
import {
FIRST_DROPDOWN_INDEX,
@@ -163,8 +164,17 @@ export default {
...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
openDropdown() {
this.showDropdown = true;
- this.isFocused = true;
- this.$emit('expandSearchBar', true);
+
+ // check isFocused state to avoid firing duplicate events
+ if (!this.isFocused) {
+ this.isFocused = true;
+ this.$emit('expandSearchBar', true);
+
+ Tracking.event(undefined, 'focus_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
+ }
},
closeDropdown() {
this.showDropdown = false;
@@ -178,6 +188,11 @@ export default {
this.showDropdown = false;
this.isFocused = false;
this.$emit('collapseSearchBar');
+
+ Tracking.event(undefined, 'blur_input', {
+ label: 'global_search',
+ property: 'top_navigation',
+ });
}, 200);
},
submitSearch() {
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index c184e25f67f..00059d01308 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -23,7 +23,12 @@ export default {
<template>
<div class="d-flex align-items-center">
- <ci-icon is-borderless :status="job.status" :size="24" class="d-flex" />
+ <ci-icon
+ is-borderless
+ :status="job.status"
+ :size="24"
+ class="gl-align-items-center gl-border gl-display-inline-flex gl-z-index-1"
+ />
<span class="gl-ml-3">
{{ job.name }}
<a
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 2284ffb8480..4d8c62d3430 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlIcon, GlTooltipDirective, GlBadge } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { __ } from '~/locale';
import Item from './item.vue';
export default {
@@ -10,7 +10,6 @@ export default {
components: {
GlIcon,
GlBadge,
- CiIcon,
Item,
GlLoadingIcon,
},
@@ -27,11 +26,15 @@ export default {
},
computed: {
collapseIcon() {
- return this.stage.isCollapsed ? 'chevron-lg-left' : 'chevron-lg-down';
+ return this.stage.isCollapsed ? 'chevron-lg-down' : 'chevron-lg-up';
},
showLoadingIcon() {
return this.stage.isLoading && !this.stage.jobs.length;
},
+ stageTitle() {
+ const prefix = __('Stage');
+ return `${prefix}: ${this.stage.name}`;
+ },
jobsCount() {
return this.stage.jobs.length;
},
@@ -57,29 +60,29 @@ export default {
<template>
<div class="ide-stage card gl-mt-3">
<div
- ref="cardHeader"
:class="{
'border-bottom-0': stage.isCollapsed,
}"
- class="card-header"
+ class="card-header gl-align-items-center gl-cursor-pointer gl-display-flex"
+ data-testid="card-header"
@click="toggleCollapsed"
>
- <ci-icon :status="stage.status" :size="24" />
<strong
ref="stageTitle"
v-gl-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
- class="gl-ml-3 text-truncate"
+ class="gl-text-truncate"
+ data-testid="stage-title"
>
- {{ stage.name }}
+ {{ stageTitle }}
</strong>
<div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2">
<gl-badge>{{ jobsCount }}</gl-badge>
</div>
- <gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" />
+ <gl-icon :name="collapseIcon" class="gl-absolute gl-right-5" />
</div>
- <div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0">
+ <div v-show="!stage.isCollapsed" class="card-body p-0" data-testid="job-list">
<gl-loading-icon v-if="showLoadingIcon" size="sm" />
<template v-else>
<item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index 9684bf8f18c..dbfaeba9708 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,7 +1,7 @@
<script>
import { GlModal, GlButton } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { modalTypes } from '../../constants';
import { trimPathComponents, getPathParent } from '../../utils';
@@ -77,7 +77,7 @@ export default {
if (this.modalType === modalTypes.rename) {
if (this.entries[this.entryName] && !this.entries[this.entryName].deleted) {
- createFlash({
+ createAlert({
message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: this.entryName,
}),
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index a1396995a3b..5f35dbdc5e7 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -11,7 +11,7 @@ import {
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
import SourceEditor from '~/editor/source_editor';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import ModelManager from '~/ide/lib/common/model_manager';
import { defaultDiffEditorOptions, defaultEditorOptions } from '~/ide/lib/editor_options';
import { __ } from '~/locale';
@@ -239,7 +239,7 @@ export default {
this.createEditorInstance();
})
.catch((err) => {
- createFlash({
+ createAlert({
message: __('Error setting up editor. Please try again.'),
fadeTransition: false,
addBodyClass: true,
@@ -331,7 +331,7 @@ export default {
useLivePreviewExtension();
})
.catch((e) =>
- createFlash({
+ createAlert({
message: e,
}),
);
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index 10e9f6a9488..1a191f6f76f 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -99,7 +99,9 @@ export function startIde(options) {
return;
}
- if (gon.features?.vscodeWebIde) {
+ const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
+
+ if (useNewWebIde) {
initGitlabWebIDE(ideElement);
} else {
resetServiceWorkersPublicPath();
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index a061da38d4f..140f2895a29 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -7,8 +7,7 @@ export const initGitlabWebIDE = async (el) => {
const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
// what: Pull what we need from the element. We will replace it soon.
- const { path_with_namespace: projectPath } = JSON.parse(el.dataset.project);
- const { cspNonce: nonce, branchName: ref } = el.dataset;
+ const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
// what: Clean up the element, but preserve id.
// why: This way we don't inherit any `ide-loading` side-effects. This
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index b22e58a376d..dc0f3a1d7e9 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@@ -36,7 +36,7 @@ export const createTempEntry = (
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (getters.entryExists(name)) {
- createFlash({
+ createAlert({
message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: name.split('/').pop(),
}),
@@ -281,7 +281,7 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
if (e.response.status === 404) {
reject(e);
} else {
- createFlash({
+ createAlert({
message: __('Error loading branch data. Please try again.'),
fadeTransition: false,
addBodyClass: true,
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index f3f603d4ae9..cd8088bf667 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants';
import service from '../../services';
@@ -34,7 +34,7 @@ export const getMergeRequestsForBranch = (
}
})
.catch((e) => {
- createFlash({
+ createAlert({
message: __(`Error fetching merge requests for ${branchId}`),
fadeTransition: false,
addBodyClass: true,
@@ -233,7 +233,7 @@ export const openMergeRequest = async (
await dispatch('openMergeRequestChanges', changes);
} catch (e) {
- createFlash({ message: __('Error while loading the merge request. Please try again.') });
+ createAlert({ message: __('Error while loading the merge request. Please try again.') });
throw e;
}
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 37a405e3fac..7a6a267e7d0 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { logError } from '~/lib/logger';
import api from '~/api';
@@ -11,7 +11,7 @@ const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.'
const errorFetchingData = (e) => {
logError(ERROR_LOADING_PROJECT, e);
- createFlash({
+ createAlert({
message: ERROR_LOADING_PROJECT,
fadeTransition: false,
addBodyClass: true,
@@ -51,7 +51,7 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {})
});
})
.catch((e) => {
- createFlash({
+ createAlert({
message: __('Error loading last commit.'),
fadeTransition: false,
addBodyClass: true,
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 2ff71523b1b..cbc6e0fe519 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { addNumericSuffix } from '~/ide/utils';
import { sprintf, __ } from '~/locale';
import { leftSidebarViews } from '../../../constants';
@@ -143,7 +143,7 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(types.UPDATE_LOADING, false);
if (!data.short_id) {
- createFlash({
+ createAlert({
message: data.message,
fadeTransition: false,
addBodyClass: true,
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 82d9300d30b..91868132a5a 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,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
@@ -26,7 +26,7 @@ export const receiveStartSessionSuccess = ({ commit, dispatch }, data) => {
};
export const receiveStartSessionError = ({ dispatch }) => {
- createFlash({ message: messages.UNEXPECTED_ERROR_STARTING });
+ createAlert({ message: messages.UNEXPECTED_ERROR_STARTING });
dispatch('killSession');
};
@@ -59,7 +59,7 @@ export const receiveStopSessionSuccess = ({ dispatch }) => {
};
export const receiveStopSessionError = ({ dispatch }) => {
- createFlash({ message: messages.UNEXPECTED_ERROR_STOPPING });
+ createAlert({ message: messages.UNEXPECTED_ERROR_STOPPING });
dispatch('killSession');
};
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
index 7fe1a8cc2df..4aa0768d394 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as messages from '../messages';
import * as types from '../mutation_types';
@@ -42,7 +42,7 @@ export const receiveSessionStatusSuccess = ({ commit, dispatch }, data) => {
};
export const receiveSessionStatusError = ({ dispatch }) => {
- createFlash({ message: messages.UNEXPECTED_ERROR_STATUS });
+ createAlert({ message: messages.UNEXPECTED_ERROR_STATUS });
dispatch('killSession');
};
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index a7e6506b045..83a3d7f2ac3 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -1,5 +1,6 @@
import { flatten, isString } from 'lodash';
import { languages } from 'monaco-editor';
+import { setDiagnosticsOptions as yamlDiagnosticsOptions } from 'monaco-yaml';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { SIDE_LEFT, SIDE_RIGHT } from './constants';
@@ -82,17 +83,16 @@ export function registerLanguages(def, ...defs) {
}
export function registerSchema(schema, options = {}) {
- const defaults = [languages.json.jsonDefaults, languages.yaml.yamlDefaults];
- defaults.forEach((d) =>
- d.setDiagnosticsOptions({
- validate: true,
- enableSchemaRequest: true,
- hover: true,
- completion: true,
- schemas: [schema],
- ...options,
- }),
- );
+ const defaultOptions = {
+ validate: true,
+ enableSchemaRequest: true,
+ hover: true,
+ completion: true,
+ schemas: [schema],
+ ...options,
+ };
+ languages.json.jsonDefaults.setDiagnosticsOptions(defaultOptions);
+ yamlDiagnosticsOptions(defaultOptions);
}
export const otherSide = (side) => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
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 98ee858ca91..0cdd64b1b98 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
@@ -12,7 +12,7 @@ import {
GlFormCheckbox,
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__, __, n__, sprintf } from '~/locale';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
@@ -150,6 +150,10 @@ export default {
},
groupsTableData() {
+ if (!this.availableNamespaces) {
+ return [];
+ }
+
return this.groups.map((group) => {
const importTarget = this.getImportTarget(group);
const status = this.getStatus(group);
@@ -232,6 +236,10 @@ export default {
version: this.bulkImportSourceGroups.versionValidation.features.sourceInstanceVersion,
});
},
+
+ pageInfo() {
+ return this.bulkImportSourceGroups?.pageInfo ?? {};
+ },
},
watch: {
@@ -342,7 +350,7 @@ export default {
variables: { importRequests },
});
} catch (error) {
- createFlash({
+ createAlert({
message: i18n.ERROR_IMPORT,
captureError: true,
error,
@@ -503,6 +511,7 @@ export default {
permissionsHelpPath: helpPagePath('user/permissions', { anchor: 'group-members-permissions' }),
popoverOptions: { title: __('What is listed here?') },
i18n,
+ LOCAL_STORAGE_KEY: 'gl-bulk-imports-status-page-size-v1',
};
</script>
@@ -696,14 +705,15 @@ export default {
/>
</template>
</gl-table>
- <pagination-bar
- v-if="hasGroups"
- :page-info="bulkImportSourceGroups.pageInfo"
- class="gl-mt-3"
- @set-page="setPage"
- @set-page-size="setPageSize"
- />
</template>
</template>
+ <pagination-bar
+ v-show="!$apollo.loading && hasGroups"
+ :page-info="pageInfo"
+ class="gl-mt-3"
+ :storage-key="$options.LOCAL_STORAGE_KEY"
+ @set-page="setPage"
+ @set-page-size="setPageSize"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
index ba0f2bb947a..6ad5e448a40 100644
--- a/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
+++ b/app/assets/javascripts/import_entities/import_groups/services/status_poller.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import Poll from '~/lib/utils/poll';
import { s__ } from '~/locale';
@@ -15,7 +15,7 @@ export class StatusPoller {
statuses.forEach((status) => updateImportStatus(status));
},
errorCallback: () =>
- createFlash({
+ createAlert({
message: s__('BulkImport|Update of import statuses with realtime changes failed'),
}),
});
diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
new file mode 100644
index 00000000000..a8fdf9b9ec5
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlAccordion, GlAccordionItem, GlAlert, GlForm, GlFormCheckbox } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAccordion,
+ GlAccordionItem,
+ GlAlert,
+ GlForm,
+ GlFormCheckbox,
+ },
+ props: {
+ stages: {
+ required: true,
+ type: Array,
+ },
+ value: {
+ required: true,
+ type: Object,
+ },
+ isInitiallyExpanded: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+<template>
+ <gl-accordion :header-level="3">
+ <gl-accordion-item
+ :title="s__('ImportProjects|Advanced import settings')"
+ :visible="isInitiallyExpanded"
+ >
+ <gl-alert variant="warning" class="gl-mb-5" :dismissible="false">{{
+ s__('ImportProjects|The more information you select, the longer it will take to import')
+ }}</gl-alert>
+ <gl-form>
+ <gl-form-checkbox
+ v-for="{ name, label, details } in stages"
+ :key="name"
+ :checked="value[name]"
+ @change="$emit('input', { ...value, [name]: $event })"
+ >
+ {{ label }}
+ <template v-if="details" #help>{{ details }} </template>
+ </gl-form-checkbox>
+ </gl-form>
+ </gl-accordion-item>
+ </gl-accordion>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 848c7361601..97a7ed4bf55 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -9,10 +9,12 @@ import {
import { mapActions, mapState, mapGetters } from 'vuex';
import { n__, __, sprintf } from '~/locale';
import ProviderRepoTableRow from './provider_repo_table_row.vue';
+import AdvancedSettings from './advanced_settings.vue';
export default {
name: 'ImportProjectsTable',
components: {
+ AdvancedSettings,
ProviderRepoTableRow,
GlLoadingIcon,
GlButton,
@@ -35,6 +37,24 @@ export default {
required: false,
default: false,
},
+ optionalStages: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isAdvancedSettingsPanelInitiallyExpanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+
+ data() {
+ return {
+ optionalStagesSelection: Object.fromEntries(
+ this.optionalStages.map(({ name }) => [name, false]),
+ ),
+ };
},
computed: {
@@ -127,7 +147,7 @@ export default {
modal-id="import-all-modal"
:title="s__('ImportProjects|Import repositories')"
:ok-title="__('Import')"
- @ok="importAll"
+ @ok="importAll({ optionalStages: optionalStagesSelection })"
>
{{
n__(
@@ -150,6 +170,13 @@ export default {
/>
</form>
</div>
+ <advanced-settings
+ v-if="optionalStages && optionalStages.length"
+ v-model="optionalStagesSelection"
+ :stages="optionalStages"
+ :is-initially-expanded="isAdvancedSettingsPanelInitiallyExpanded"
+ class="gl-mb-5"
+ />
<div v-if="repositories.length" class="gl-w-full">
<table>
<thead class="gl-border-0 gl-border-solid gl-border-t-1 gl-border-gray-100">
@@ -171,6 +198,7 @@ export default {
:repo="repo"
:available-namespaces="namespaces"
:user-namespace="defaultTargetNamespace"
+ :optional-stages="optionalStagesSelection"
/>
</template>
</tbody>
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 e4090a378e1..458e0fb1cb1 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
@@ -43,6 +43,10 @@ export default {
type: Array,
required: true,
},
+ optionalStages: {
+ type: Object,
+ required: true,
+ },
},
computed: {
@@ -177,7 +181,7 @@ export default {
v-if="isImportNotStarted"
type="button"
data-qa-selector="import_button"
- @click="fetchImport(repo.importSource.id)"
+ @click="fetchImport({ repoId: repo.importSource.id, optionalStages })"
>
{{ importButtonText }}
</gl-button>
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 5146a0eb461..4daa9e8a1b8 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -42,6 +42,7 @@ export function initPropsFromElement(element) {
providerTitle: element.dataset.provider,
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
+ optionalStages: JSON.parse(element.dataset.optionalStages),
};
}
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index 92be028b8a9..a30c14f9d28 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -43,11 +43,14 @@ const restartJobsPolling = () => {
const setImportTarget = ({ commit }, { repoId, importTarget }) =>
commit(types.SET_IMPORT_TARGET, { repoId, importTarget });
-const importAll = ({ state, dispatch }) => {
+const importAll = ({ state, dispatch }, config = {}) => {
return Promise.all(
- state.repositories
- .filter(isProjectImportable)
- .map((r) => dispatch('fetchImport', r.importSource.id)),
+ state.repositories.filter(isProjectImportable).map((r) =>
+ dispatch('fetchImport', {
+ repoId: r.importSource.id,
+ optionalStages: config?.optionalStages,
+ }),
+ ),
);
};
@@ -73,7 +76,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else if (tooManyRequests(e)) {
- createFlash({
+ createAlert({
message: sprintf(s__('ImportProjects|%{provider} rate limit exceeded. Try again later'), {
provider: capitalizeFirstCharacter(provider),
}),
@@ -81,7 +84,7 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
commit(types.RECEIVE_REPOS_ERROR);
} else {
- createFlash({
+ createAlert({
message: sprintf(s__('ImportProjects|Requesting your %{provider} repositories failed'), {
provider,
}),
@@ -92,7 +95,10 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
});
};
-const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, getters }, repoId) => {
+const fetchImportFactory = (importPath = isRequired()) => (
+ { state, commit, getters },
+ { repoId, optionalStages },
+) => {
const { ciCdOnly } = state;
const importTarget = getters.getImportTarget(repoId);
@@ -105,6 +111,7 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
ci_cd_only: ciCdOnly,
new_name: newName,
target_namespace: targetNamespace,
+ ...(Object.keys(optionalStages).length ? { optional_stages: optionalStages } : {}),
})
.then(({ data }) => {
commit(types.RECEIVE_IMPORT_SUCCESS, {
@@ -124,7 +131,7 @@ const fetchImportFactory = (importPath = isRequired()) => ({ state, commit, gett
)
: s__('ImportProjects|Importing the project failed');
- createFlash({
+ createAlert({
message: flashMessage,
});
@@ -149,7 +156,7 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else {
- createFlash({
+ createAlert({
message: s__('ImportProjects|Update of imported projects with realtime changes failed'),
});
}
@@ -177,7 +184,7 @@ const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) =
commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
)
.catch(() => {
- createFlash({
+ createAlert({
message: s__('ImportProjects|Requesting namespaces failed'),
});
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 437bcc39886..2806b785816 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -91,3 +91,5 @@ export const placeholderForType = {
[INTEGRATION_TYPE_SLACK]: __('#general, #development'),
[INTEGRATION_TYPE_MATTERMOST]: __('my-channel'),
};
+
+export const INTEGRATION_FORM_TYPE_SLACK = 'gitlab_slack_application';
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 7a6f1a953a8..15f76c16516 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -14,12 +14,14 @@ import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
+ INTEGRATION_FORM_TYPE_SLACK,
integrationLevels,
integrationFormSectionComponents,
billingPlanNames,
} from '~/integrations/constants';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import csrf from '~/lib/utils/csrf';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
import ConfirmationModal from './confirmation_modal.vue';
@@ -65,6 +67,7 @@ export default {
GlModal: GlModalDirective,
SafeHtml,
},
+ mixins: [glFeatureFlagsMixin()],
inject: {
helpHtml: {
default: '',
@@ -101,6 +104,9 @@ export default {
return Boolean(this.isSaving || this.isResetting || this.isTesting);
},
hasSections() {
+ if (this.hasSlackNotificationsDisabled) {
+ return false;
+ }
return this.customState.sections.length !== 0;
},
fieldsWithoutSection() {
@@ -108,6 +114,24 @@ export default {
? this.propsSource.fields.filter((field) => !field.section)
: this.propsSource.fields;
},
+ hasFieldsWithoutSection() {
+ if (this.hasSlackNotificationsDisabled) {
+ return false;
+ }
+ return this.fieldsWithoutSection.length;
+ },
+ isSlackIntegration() {
+ return this.propsSource.type === INTEGRATION_FORM_TYPE_SLACK;
+ },
+ hasSlackNotificationsDisabled() {
+ return this.isSlackIntegration && !this.glFeatures.integrationSlackAppNotifications;
+ },
+ showHelpHtml() {
+ if (this.isSlackIntegration) {
+ return this.helpHtml;
+ }
+ return !this.hasSections && this.helpHtml;
+ },
},
methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']),
@@ -227,6 +251,31 @@ export default {
@change="setOverride"
/>
+ <section v-if="showHelpHtml" class="gl-lg-display-flex gl-justify-content-end gl-mb-6">
+ <!-- helpHtml is trusted input -->
+ <div
+ v-safe-html:[$options.helpHtmlConfig]="helpHtml"
+ data-testid="help-html"
+ class="gl-flex-basis-two-thirds"
+ ></div>
+ </section>
+
+ <section v-if="!hasSections" class="gl-lg-display-flex gl-justify-content-end">
+ <div class="gl-flex-basis-two-thirds">
+ <active-checkbox
+ v-if="propsSource.showActive"
+ :key="`${currentKey}-active-checkbox`"
+ @toggle-integration-active="onToggleIntegrationState"
+ />
+ <trigger-fields
+ v-if="propsSource.triggerEvents.length"
+ :key="`${currentKey}-trigger-fields`"
+ :events="propsSource.triggerEvents"
+ :type="propsSource.type"
+ />
+ </div>
+ </section>
+
<template v-if="hasSections">
<div
v-for="(section, index) in customState.sections"
@@ -234,8 +283,8 @@ export default {
:class="{ 'gl-border-b gl-pb-3 gl-mb-6': index !== customState.sections.length - 1 }"
data-testid="integration-section"
>
- <div class="row">
- <div class="col-lg-4">
+ <section class="gl-lg-display-flex">
+ <div class="gl-flex-basis-third gl-mr-4">
<h4 class="gl-mt-0">
{{ section.title
}}<gl-badge
@@ -253,7 +302,7 @@ export default {
<p v-safe-html="section.description"></p>
</div>
- <div class="col-lg-8">
+ <div class="gl-flex-basis-two-thirds">
<component
:is="$options.integrationFormSectionComponents[section.type]"
:fields="fieldsForSection(section)"
@@ -262,28 +311,12 @@ export default {
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
</div>
- </div>
+ </section>
</div>
</template>
- <div class="row">
- <div class="col-lg-4"></div>
-
- <div class="col-lg-8">
- <!-- helpHtml is trusted input -->
- <div v-if="helpHtml && !hasSections" v-safe-html:[$options.helpHtmlConfig]="helpHtml"></div>
-
- <active-checkbox
- v-if="propsSource.showActive && !hasSections"
- :key="`${currentKey}-active-checkbox`"
- @toggle-integration-active="onToggleIntegrationState"
- />
- <trigger-fields
- v-if="propsSource.triggerEvents.length && !hasSections"
- :key="`${currentKey}-trigger-fields`"
- :events="propsSource.triggerEvents"
- :type="propsSource.type"
- />
+ <section v-if="hasFieldsWithoutSection" class="gl-lg-display-flex gl-justify-content-end">
+ <div class="gl-flex-basis-two-thirds">
<dynamic-field
v-for="field in fieldsWithoutSection"
:key="`${currentKey}-${field.name}`"
@@ -292,12 +325,12 @@ export default {
:data-qa-selector="`${field.name}_div`"
/>
</div>
- </div>
+ </section>
- <div v-if="isEditable" class="row">
- <div :class="hasSections ? 'col' : 'col-lg-8 offset-lg-4'">
+ <section v-if="isEditable" :class="!hasSections && 'gl-lg-display-flex gl-justify-content-end'">
+ <div :class="!hasSections && 'gl-flex-basis-two-thirds'">
<div
- class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"
+ class="footer-block row-content-block gl-lg-display-flex gl-justify-content-space-between"
>
<div>
<template v-if="isInstanceOrGroupLevel">
@@ -359,6 +392,6 @@ export default {
</template>
</div>
</div>
- </div>
+ </section>
</gl-form>
</template>
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
deleted file mode 100644
index 243d82f55aa..00000000000
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import $ from 'jquery';
-import { loadCSSFile } from '../lib/utils/css_utils';
-
-let instanceCount = 0;
-
-class AutoWidthDropdownSelect {
- constructor(selectElement) {
- this.$selectElement = $(selectElement);
- this.dropdownClass = `js-auto-width-select-dropdown-${instanceCount}`;
- instanceCount += 1;
- }
-
- init() {
- const { dropdownClass } = this;
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- this.$selectElement.select2({
- dropdownCssClass: dropdownClass,
- ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass),
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
-
- return this;
- }
-
- static selectOptions(dropdownClass) {
- return {
- dropdownCss() {
- let resultantWidth = 'auto';
- const $dropdown = $(`.${dropdownClass}`);
-
- // We have to look at the parent because
- // `offsetParent` on a `display: none;` is `null`
- const offsetParentWidth = $(this).parent().offsetParent().width();
- // Reset any width to let it naturally flow
- $dropdown.css('width', 'auto');
- if ($dropdown.outerWidth(false) > offsetParentWidth) {
- resultantWidth = offsetParentWidth;
- }
-
- return {
- width: resultantWidth,
- maxWidth: offsetParentWidth,
- };
- },
- };
- }
-}
-
-export default AutoWidthDropdownSelect;
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue
index 9509399e91d..ba94932289e 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_select.vue
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue
@@ -1,10 +1,9 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
-import { ISSUE_STATUS_SELECT_OPTIONS } from '../constants';
+import { statusDropdownOptions } from '../constants';
export default {
- name: 'StatusSelect',
components: {
GlDropdown,
GlDropdownItem,
@@ -36,7 +35,7 @@ export default {
dropdownTitle: __('Change status'),
defaultDropdownText: __('Select status'),
},
- ISSUE_STATUS_SELECT_OPTIONS,
+ statusDropdownOptions,
};
</script>
<template>
@@ -44,7 +43,7 @@ export default {
<input type="hidden" name="update[state_event]" :value="selectedValue" />
<gl-dropdown :text="dropdownText" :title="$options.i18n.dropdownTitle" class="gl-w-full">
<gl-dropdown-item
- v-for="statusOption in $options.ISSUE_STATUS_SELECT_OPTIONS"
+ v-for="statusOption in $options.statusDropdownOptions"
:key="statusOption.value"
:is-checked="selectedValue === statusOption.value"
is-check-item
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue
new file mode 100644
index 00000000000..8774b065c22
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { subscriptionsDropdownOptions } from '../constants';
+
+export default {
+ subscriptionsDropdownOptions,
+ i18n: {
+ defaultDropdownText: __('Select subscription'),
+ headerText: __('Change subscription'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ data() {
+ return {
+ subscription: undefined,
+ };
+ },
+ computed: {
+ dropdownText() {
+ return this.subscription?.text ?? this.$options.i18n.defaultDropdownText;
+ },
+ selectedValue() {
+ return this.subscription?.value;
+ },
+ },
+ methods: {
+ handleClick(option) {
+ this.subscription = option.value === this.subscription?.value ? undefined : option;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <input type="hidden" name="update[subscription_event]" :value="selectedValue" />
+ <gl-dropdown class="gl-w-full" :header-text="$options.i18n.headerText" :text="dropdownText">
+ <gl-dropdown-item
+ v-for="subscriptionsOption in $options.subscriptionsDropdownOptions"
+ :key="subscriptionsOption.value"
+ is-check-item
+ :is-checked="selectedValue === subscriptionsOption.value"
+ @click="handleClick(subscriptionsOption)"
+ >
+ {{ subscriptionsOption.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
index ad15b25f9cf..68133ceb3c7 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
@@ -1,17 +1,23 @@
import { __ } from '~/locale';
-export const ISSUE_STATUS_MODIFIERS = {
- REOPEN: 'reopen',
- CLOSE: 'close',
-};
-
-export const ISSUE_STATUS_SELECT_OPTIONS = [
+export const statusDropdownOptions = [
{
- value: ISSUE_STATUS_MODIFIERS.REOPEN,
text: __('Open'),
+ value: 'reopen',
},
{
- value: ISSUE_STATUS_MODIFIERS.CLOSE,
text: __('Closed'),
+ value: 'close',
+ },
+];
+
+export const subscriptionsDropdownOptions = [
+ {
+ text: __('Subscribe'),
+ value: 'subscribe',
+ },
+ {
+ text: __('Unsubscribe'),
+ value: 'unsubscribe',
},
];
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
index 967996b859e..4657771353f 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
-import StatusSelect from './components/status_select.vue';
+import StatusDropdown from './components/status_dropdown.vue';
+import SubscriptionsDropdown from './components/subscriptions_dropdown.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
@@ -14,8 +15,8 @@ export function initBulkUpdateSidebar(prefixId) {
new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
}
-export function initIssueStatusSelect() {
- const el = document.querySelector('.js-issue-status');
+export function initStatusDropdown() {
+ const el = document.querySelector('.js-status-dropdown');
if (!el) {
return null;
@@ -23,7 +24,21 @@ export function initIssueStatusSelect() {
return new Vue({
el,
- name: 'StatusSelectRoot',
- render: (createElement) => createElement(StatusSelect),
+ name: 'StatusDropdownRoot',
+ render: (createElement) => createElement(StatusDropdown),
+ });
+}
+
+export function initSubscriptionsDropdown() {
+ const el = document.querySelector('.js-subscriptions-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'SubscriptionsDropdownRoot',
+ render: (createElement) => createElement(SubscriptionsDropdown),
});
}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
index 8a55176fed0..a33c6ae8030 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -5,7 +5,6 @@ import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import subscriptionSelect from './subscription_select';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
@@ -52,7 +51,6 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- subscriptionSelect();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js b/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
deleted file mode 100644
index b12ac776b4f..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/subscription_select.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { __ } from '~/locale';
-
-export default function subscriptionSelect() {
- $('.js-subscription-event').each((i, element) => {
- const fieldName = $(element).data('fieldName');
-
- return initDeprecatedJQueryDropdown($(element), {
- selectable: true,
- fieldName,
- toggleLabel(selected, el, instance) {
- let label = __('Subscription');
- const $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- },
- clicked(options) {
- return options.e.preventDefault();
- },
- id(obj, el) {
- return $(el).data('id');
- },
- });
- });
-}
diff --git a/app/assets/javascripts/issuable/issuable_context.js b/app/assets/javascripts/issuable/issuable_context.js
index 37001d00a27..8c2e2a5df67 100644
--- a/app/assets/javascripts/issuable/issuable_context.js
+++ b/app/assets/javascripts/issuable/issuable_context.js
@@ -1,7 +1,6 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
-import { loadCSSFile } from '~/lib/utils/css_utils';
import UsersSelect from '~/users_select';
export default class IssuableContext {
@@ -9,24 +8,6 @@ export default class IssuableContext {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
- const $select2 = $('select.select2');
-
- if ($select2.length) {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- $select2.select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
- }
-
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
});
diff --git a/app/assets/javascripts/issuable/issuable_form.js b/app/assets/javascripts/issuable/issuable_form.js
index 81bf7ca6ccc..e8ba99e0e9e 100644
--- a/app/assets/javascripts/issuable/issuable_form.js
+++ b/app/assets/javascripts/issuable/issuable_form.js
@@ -2,10 +2,7 @@ import $ from 'jquery';
import Pikaday from 'pikaday';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import Autosave from '~/autosave';
-import AutoWidthDropdownSelect from '~/issuable/auto_width_dropdown_select';
-import { loadCSSFile } from '~/lib/utils/css_utils';
import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
-import { select2AxiosTransport } from '~/lib/utils/select2_utils';
import { queryToObject, objectToQuery } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import ZenMode from '~/zen_mode';
@@ -118,12 +115,6 @@ export default class IssuableForm {
});
calendar.setDate(parsePikadayDate($issuableDueDate.val()));
}
-
- this.$targetBranchSelect = $('.js-target-branch-select', this.form);
-
- if (this.$targetBranchSelect.length) {
- this.initTargetBranchDropdown();
- }
}
initAutosave() {
@@ -214,47 +205,4 @@ export default class IssuableForm {
addWip() {
this.titleField.val(`Draft: ${this.titleField.val()}`);
}
-
- initTargetBranchDropdown() {
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- this.$targetBranchSelect.select2({
- ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'),
- ajax: {
- url: this.$targetBranchSelect.data('endpoint'),
- dataType: 'JSON',
- quietMillis: 250,
- data(search) {
- return {
- search,
- };
- },
- results({ results }) {
- return {
- // `data` keys are translated so we can't just access them with a string based key
- results: results[Object.keys(results)[0]].map((name) => ({
- id: name,
- text: name,
- })),
- };
- },
- transport: select2AxiosTransport,
- },
- initSelection(el, callback) {
- const val = el.val();
-
- callback({
- id: val,
- text: val,
- });
- },
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
- }
}
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 0b424d105b9..acb6aa93f0f 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -247,8 +247,8 @@ export default {
},
defaultWorkItemTypes() {
return this.isWorkItemsEnabled
- ? defaultWorkItemTypes.concat(WORK_ITEM_TYPE_ENUM_TASK)
- : defaultWorkItemTypes;
+ ? defaultWorkItemTypes
+ : defaultWorkItemTypes.filter((type) => type !== WORK_ITEM_TYPE_ENUM_TASK);
},
typeTokenOptions() {
return this.isWorkItemsEnabled
@@ -563,7 +563,8 @@ export default {
if (!this.hasInitBulkEdit) {
const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
- bulkUpdateSidebar.initIssueStatusSelect();
+ bulkUpdateSidebar.initStatusDropdown();
+ bulkUpdateSidebar.initSubscriptionsDropdown();
const usersSelect = await import('~/users_select');
const UsersSelect = usersSelect.default;
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 27738d7a3e6..9fe8899ab39 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -7,11 +7,13 @@ import {
FILTER_UPCOMING,
OPERATOR_IS,
OPERATOR_IS_NOT,
+ TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
} from '~/work_items/constants';
export const i18n = {
@@ -147,14 +149,16 @@ export const TOKEN_TYPE_EPIC = 'epic_id';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_CONTACT = 'crm_contact';
export const TOKEN_TYPE_ORGANIZATION = 'crm_organization';
-export const TOKEN_TYPE_HEALTH = 'health_status';
-export const TYPE_TOKEN_TASK_OPTION = { icon: 'task-done', title: 'task', value: 'task' };
+export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
+// This should be consistent with Issue::TYPES_FOR_LIST in the backend
+// https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/models/issue.rb#L48
export const defaultWorkItemTypes = [
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_TEST_CASE,
+ WORK_ITEM_TYPE_ENUM_TASK,
];
export const defaultTypeTokenOptions = [
@@ -327,10 +331,12 @@ export const filters = {
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'healthStatus',
+ [SPECIAL_FILTER]: 'healthStatus',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
+ [SPECIAL_FILTER]: 'health_status',
},
},
},
diff --git a/app/assets/javascripts/issues/show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue
index 4c5f783cd66..5138a4530e9 100644
--- a/app/assets/javascripts/issues/show/components/edited.vue
+++ b/app/assets/javascripts/issues/show/components/edited.vue
@@ -1,10 +1,11 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
components: {
TimeAgoTooltip,
+ GlSprintf,
},
props: {
updatedAt: {
@@ -33,13 +34,27 @@ export default {
<template>
<small class="edited-text js-issue-widgets">
- Edited
- <time-ago-tooltip v-if="updatedAt" :time="updatedAt" tooltip-placement="bottom" />
- <span v-if="hasUpdatedBy">
- by
- <a :href="updatedByPath" class="author-link">
- <span>{{ updatedByName }}</span>
- </a>
- </span>
+ <gl-sprintf v-if="!hasUpdatedBy" :message="__('Edited %{timeago}')">
+ <template #timeago>
+ <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" />
+ </template>
+ </gl-sprintf>
+ <gl-sprintf v-else-if="!updatedAt" :message="__('Edited by %{author}')">
+ <template #author>
+ <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline">
+ <span>{{ updatedByName }}</span>
+ </a>
+ </template>
+ </gl-sprintf>
+ <gl-sprintf v-else :message="__('Edited %{timeago} by %{author}')">
+ <template #timeago>
+ <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" />
+ </template>
+ <template #author>
+ <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline">
+ <span>{{ updatedByName }}</span>
+ </a>
+ </template>
+ </gl-sprintf>
</small>
</template>
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index c2ab7c4f298..dbe634e7295 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -1,13 +1,16 @@
<script>
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import updateMixin from '../../mixins/update';
export default {
components: {
MarkdownField,
+ MarkdownEditor,
},
- mixins: [updateMixin],
+ mixins: [updateMixin, glFeaturesFlagMixin()],
props: {
value: {
type: String,
@@ -38,7 +41,12 @@ export default {
},
},
mounted() {
- this.$refs.textarea.focus();
+ this.focus();
+ },
+ methods: {
+ focus() {
+ this.$refs.textarea?.focus();
+ },
},
};
</script>
@@ -46,7 +54,26 @@ export default {
<template>
<div class="common-note-form">
<label class="sr-only" for="issue-description">{{ __('Description') }}</label>
+ <markdown-editor
+ v-if="glFeatures.contentEditorOnIssues"
+ class="gl-mt-3"
+ :value="value"
+ :render-markdown-path="markdownPreviewPath"
+ :markdown-docs-path="markdownDocsPath"
+ :form-field-aria-label="__('Description')"
+ :form-field-placeholder="__('Write a comment or drag your files here…')"
+ form-field-id="issue-description"
+ form-field-name="issue-description"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :enable-autocomplete="enableAutocomplete"
+ supports-quick-actions
+ init-on-autofocus
+ @input="$emit('input', $event)"
+ @keydown.meta.enter="updateIssuable"
+ @keydown.ctrl.enter="updateIssuable"
+ />
<markdown-field
+ v-else
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index f479c8ae78d..0c6b61fb893 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import $ from 'jquery';
-import Autosave from '~/autosave';
+import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave';
import { IssuableType } from '~/issues/constants';
import eventHub from '../event_hub';
import EditActions from './edit_actions.vue';
@@ -76,10 +75,17 @@ export default {
},
},
data() {
+ const autosaveKey = [document.location.pathname, document.location.search];
+ const descriptionAutosaveKey = [...autosaveKey, 'description'];
+ const titleAutosaveKey = [...autosaveKey, 'title'];
+
return {
+ titleAutosaveKey,
+ descriptionAutosaveKey,
+ autosaveReset: false,
formData: {
- title: this.formState.title,
- description: this.formState.description,
+ title: getDraft(titleAutosaveKey) || this.formState.title,
+ description: getDraft(descriptionAutosaveKey) || this.formState.description,
},
showOutdatedDescriptionWarning: false,
};
@@ -118,58 +124,40 @@ export default {
},
methods: {
initAutosave() {
- const {
- description: {
- $refs: { textarea },
- },
- title: {
- $refs: { input },
- },
- } = this.$refs;
-
- this.autosaveDescription = new Autosave(
- $(textarea),
- [document.location.pathname, document.location.search, 'description'],
- null,
- this.formState.lock_version,
- );
-
- const savedLockVersion = this.autosaveDescription.getSavedLockVersion();
+ const savedLockVersion = getLockVersion(this.descriptionAutosaveKey);
this.showOutdatedDescriptionWarning =
savedLockVersion && String(this.formState.lock_version) !== savedLockVersion;
-
- this.autosaveTitle = new Autosave($(input), [
- document.location.pathname,
- document.location.search,
- 'title',
- ]);
},
resetAutosave() {
- this.autosaveDescription.reset();
- this.autosaveTitle.reset();
+ this.autosaveReset = true;
+ clearDraft(this.descriptionAutosaveKey);
+ clearDraft(this.titleAutosaveKey);
},
keepAutosave() {
- const {
- description: {
- $refs: { textarea },
- },
- } = this.$refs;
-
- textarea.focus();
+ this.$refs.description.focus();
this.showOutdatedDescriptionWarning = false;
},
discardAutosave() {
- const {
- description: {
- $refs: { textarea },
- },
- } = this.$refs;
-
- textarea.value = this.initialDescriptionText;
- textarea.focus();
+ this.formData.description = this.initialDescriptionText;
+ clearDraft(this.descriptionAutosaveKey);
+ this.$refs.description.focus();
this.showOutdatedDescriptionWarning = false;
},
+ updateTitleDraft(title) {
+ updateDraft(this.titleAutosaveKey, title);
+ },
+ updateDescriptionDraft(description) {
+ /*
+ * This conditional statement prevents a race-condition
+ * between clearing the draft and submitting a new draft
+ * update while the user is typing. It happens when saving
+ * using the cmd + enter keyboard shortcut.
+ */
+ if (!this.autosaveReset) {
+ updateDraft(this.descriptionAutosaveKey, description, this.formState.lock_version);
+ }
+ },
},
};
</script>
@@ -194,7 +182,7 @@ export default {
>
<div class="row gl-mb-3">
<div class="col-12">
- <issuable-title-field ref="title" v-model="formData.title" />
+ <issuable-title-field ref="title" v-model="formData.title" @input="updateTitleDraft" />
</div>
</div>
<div class="row">
@@ -220,6 +208,7 @@ export default {
:markdown-docs-path="markdownDocsPath"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
+ @input="updateDescriptionDraft"
/>
<edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" />
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index adf449aca7b..74d166f82bb 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -229,7 +229,7 @@ export default {
</script>
<template>
- <div class="detail-page-header-actions gl-display-flex">
+ <div class="detail-page-header-actions gl-display-flex gl-align-self-start">
<gl-dropdown
v-if="hasMobileDropdown"
class="gl-sm-display-none! w-100"
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index dd84a1d7d67..5725d0f8d6a 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -52,9 +52,6 @@ export default {
loading() {
return this.$apollo.queries.alert.loading;
},
- incidentTabEnabled() {
- return this.glFeatures.incidentTimeline;
- },
},
mounted() {
this.trackPageViews();
@@ -112,7 +109,7 @@ export default {
>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <timeline-tab v-if="incidentTabEnabled" />
+ <timeline-tab />
</gl-tabs>
</div>
</template>
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 b7ae18372ab..55cd8b5f606 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
@@ -74,6 +74,9 @@ export default {
return utcDate.toISOString();
},
+ hasTimelineText() {
+ return this.timelineText.length > 0;
+ },
},
mounted() {
this.focusDate();
@@ -167,6 +170,8 @@ export default {
variant="confirm"
category="primary"
class="gl-mr-3"
+ data-testid="save-button"
+ :disabled="!hasTimelineText"
:loading="isEventProcessed"
@click="handleSave(false)"
>
@@ -177,6 +182,8 @@ export default {
variant="confirm"
category="secondary"
class="gl-mr-3 gl-ml-n2"
+ data-testid="save-and-add-button"
+ :disabled="!hasTimelineText"
:loading="isEventProcessed"
@click="handleSave(true)"
>
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index e5eed9f6b79..3cb5007ab0d 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -60,6 +60,7 @@ export function initIncidentApp(issueData = {}) {
projectId,
slaFeatureAvailable: parseBoolean(slaFeatureAvailable),
uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
+ contentEditorOnIssues: gon.features.contentEditorOnIssues,
},
render(createElement) {
return createElement(IssueApp, {
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js
index 853834ed51d..f73241aed6b 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/jobs/components/table/constants.js
@@ -1,5 +1,4 @@
import { s__, __ } from '~/locale';
-import { DEFAULT_TH_CLASSES } from '~/lib/utils/constants';
/* Error constants */
export const POST_FAILURE = 'post_failure';
@@ -29,62 +28,46 @@ export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
/* Table constants */
-
-const defaultTableClasses = {
- tdClass: 'gl-p-5!',
- thClass: DEFAULT_TH_CLASSES,
-};
-// eslint-disable-next-line @gitlab/require-i18n-strings
-const coverageTdClasses = `${defaultTableClasses.tdClass} gl-display-none! gl-lg-display-table-cell!`;
-
export const DEFAULT_FIELDS = [
{
key: 'status',
label: __('Status'),
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'job',
label: __('Job'),
- ...defaultTableClasses,
columnClass: 'gl-w-20p',
},
{
key: 'pipeline',
label: __('Pipeline'),
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'stage',
label: __('Stage'),
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
{
key: 'name',
label: __('Name'),
- ...defaultTableClasses,
columnClass: 'gl-w-15p',
},
{
key: 'duration',
label: __('Duration'),
- ...defaultTableClasses,
columnClass: 'gl-w-15p',
},
{
key: 'coverage',
label: __('Coverage'),
- tdClass: coverageTdClasses,
- thClass: defaultTableClasses.thClass,
+ tdClass: 'gl-display-none! gl-lg-display-table-cell!',
columnClass: 'gl-w-10p',
},
{
key: 'actions',
label: '',
- ...defaultTableClasses,
columnClass: 'gl-w-10p',
},
];
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index 0a4757d11a8..3209fc4b90d 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility';
import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue';
import { validateQueryString } from '../filtered_search/utils';
@@ -134,7 +134,7 @@ export default {
// when a user enters raw text we alert them that it is
// not supported and we do not make an additional API call
if (!filter.type) {
- createFlash({
+ createAlert({
message: RAW_TEXT_WARNING,
type: 'warning',
});
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index 927ba7c7e1e..272181f830c 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+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';
@@ -100,7 +100,7 @@ export const receiveJobSuccess = ({ commit }, data = {}) => {
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
- createFlash({
+ createAlert({
message: __('An error occurred while fetching the job.'),
});
resetFavicon();
@@ -205,14 +205,14 @@ export const receiveJobLogSuccess = ({ commit }, log) => commit(types.RECEIVE_JO
export const receiveJobLogError = ({ dispatch }) => {
dispatch('stopPollingJobLog');
- createFlash({
+ createAlert({
message: __('An error occurred while fetching the job log.'),
});
};
export const receiveJobLogUnauthorizedError = ({ dispatch }) => {
dispatch('stopPollingJobLog');
- createFlash({
+ createAlert({
message: __('The current user is not authorized to access the job log.'),
});
};
@@ -254,7 +254,7 @@ export const receiveJobsForStageSuccess = ({ commit }, data) =>
export const receiveJobsForStageError = ({ commit }) => {
commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
- createFlash({
+ createAlert({
message: __('An error occurred while fetching the jobs.'),
});
};
@@ -271,7 +271,7 @@ export const triggerManualJob = ({ state }, variables) => {
job_variables_attributes: parsedVariables,
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while triggering the job.'),
}),
);
diff --git a/app/assets/javascripts/labels/components/promote_label_modal.vue b/app/assets/javascripts/labels/components/promote_label_modal.vue
index 8598500c842..1b99a094c48 100644
--- a/app/assets/javascripts/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/labels/components/promote_label_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlSprintf, GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
@@ -70,7 +70,7 @@ export default {
labelUrl: this.url,
successful: false,
});
- createFlash({
+ createAlert({
message: error,
});
});
diff --git a/app/assets/javascripts/labels/group_label_subscription.js b/app/assets/javascripts/labels/group_label_subscription.js
index ea69e6585e6..c4f80d32a83 100644
--- a/app/assets/javascripts/labels/group_label_subscription.js
+++ b/app/assets/javascripts/labels/group_label_subscription.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
import { fixTitle, hide } from '~/tooltips';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
const tooltipTitles = {
@@ -31,7 +31,7 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.removeAttr('data-url');
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was an error when unsubscribing from this label.'),
}),
);
@@ -50,7 +50,7 @@ export default class GroupLabelSubscription {
.then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was an error when subscribing to this label.'),
}),
);
diff --git a/app/assets/javascripts/labels/label_manager.js b/app/assets/javascripts/labels/label_manager.js
index 1927ac6e1ec..be515869bff 100644
--- a/app/assets/javascripts/labels/label_manager.js
+++ b/app/assets/javascripts/labels/label_manager.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import { dispose } from '~/tooltips';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -112,7 +112,7 @@ export default class LabelManager {
onPrioritySortUpdate() {
this.savePrioritySort().catch(() =>
- createFlash({
+ createAlert({
message: this.errorMessage,
}),
);
@@ -127,7 +127,7 @@ export default class LabelManager {
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
- createFlash({
+ createAlert({
message: this.errorMessage,
});
}
diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 51fedac339b..65dda804a20 100644
--- a/app/assets/javascripts/labels/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -6,7 +6,7 @@ import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import IssuableBulkUpdateActions from '~/issuable/bulk_update_sidebar/issuable_bulk_update_actions';
import { isScopedLabel } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sprintf, __ } from '~/locale';
import CreateLabelDropdown from './create_label_dropdown';
@@ -146,7 +146,7 @@ export default class LabelsSelect {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error saving label update.'),
}),
);
@@ -185,7 +185,7 @@ export default class LabelsSelect {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching labels.'),
}),
);
diff --git a/app/assets/javascripts/labels/project_label_subscription.js b/app/assets/javascripts/labels/project_label_subscription.js
index b2612e9ede0..9ca6ee5609c 100644
--- a/app/assets/javascripts/labels/project_label_subscription.js
+++ b/app/assets/javascripts/labels/project_label_subscription.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { fixTitle } from '~/tooltips';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -61,7 +61,7 @@ export default class ProjectLabelSubscription {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was an error subscribing to this label.'),
}),
);
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 6f24590f9e7..27760e483aa 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -3,12 +3,21 @@ import { getNormalizedURL, getBaseURL, relativePathToAbsolute } from '~/lib/util
const { sanitize: dompurifySanitize, addHook, isValidAttribute } = DOMPurify;
-const defaultConfig = {
+export const defaultConfig = {
// Safely allow SVG <use> tags
ADD_TAGS: ['use', 'gl-emoji', 'copy-code'],
// Prevent possible XSS attacks with data-* attributes used by @rails/ujs
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421
- FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'],
+ FORBID_ATTR: [
+ 'data-remote',
+ 'data-url',
+ 'data-type',
+ 'data-method',
+ 'data-disable-with',
+ 'data-disabled',
+ 'data-disable',
+ 'data-turbo',
+ ],
FORBID_TAGS: ['style', 'mstyle'],
ALLOW_UNKNOWN_PROTOCOLS: true,
};
diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js
index dac1da743a2..01316be06a2 100644
--- a/app/assets/javascripts/lib/utils/autosave.js
+++ b/app/assets/javascripts/lib/utils/autosave.js
@@ -1,8 +1,27 @@
+import { isString } from 'lodash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+const normalizeKey = (autosaveKey) => {
+ let normalizedKey;
+
+ if (Array.isArray(autosaveKey) && autosaveKey.every(isString)) {
+ normalizedKey = autosaveKey.join('/');
+ } else if (isString(autosaveKey)) {
+ normalizedKey = autosaveKey;
+ } else {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Invalid autosave key');
+ }
+
+ return `autosave/${normalizedKey}`;
+};
+
+const lockVersionKey = (autosaveKey) => `${normalizeKey(autosaveKey)}/lockVersion`;
+
export const clearDraft = (autosaveKey) => {
try {
- window.localStorage.removeItem(`autosave/${autosaveKey}`);
+ window.localStorage.removeItem(normalizeKey(autosaveKey));
+ window.localStorage.removeItem(lockVersionKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
@@ -11,7 +30,17 @@ export const clearDraft = (autosaveKey) => {
export const getDraft = (autosaveKey) => {
try {
- return window.localStorage.getItem(`autosave/${autosaveKey}`);
+ return window.localStorage.getItem(normalizeKey(autosaveKey));
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ return null;
+ }
+};
+
+export const getLockVersion = (autosaveKey) => {
+ try {
+ return window.localStorage.getItem(lockVersionKey(autosaveKey));
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
@@ -19,9 +48,12 @@ export const getDraft = (autosaveKey) => {
}
};
-export const updateDraft = (autosaveKey, text) => {
+export const updateDraft = (autosaveKey, text, lockVersion) => {
try {
- window.localStorage.setItem(`autosave/${autosaveKey}`, text);
+ window.localStorage.setItem(normalizeKey(autosaveKey), text);
+ if (lockVersion) {
+ window.localStorage.setItem(lockVersionKey(autosaveKey), lockVersion);
+ }
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 7925a10344a..4448a106bb6 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -60,6 +60,15 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa
});
};
+/**
+ * Return the given element's offset height, or 0 if the element doesn't exist.
+ * Probably not useful outside of handleLocationHash.
+ *
+ * @param {HTMLElement} element The element to measure.
+ * @returns {number} The element's offset height.
+ */
+const getElementOffsetHeight = (element) => element?.offsetHeight ?? 0;
+
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => {
@@ -84,40 +93,26 @@ export const handleLocationHash = () => {
const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
let adjustment = 0;
- if (fixedNav) adjustment -= fixedNav.offsetHeight;
-
- if (target && target.scrollIntoView) {
- target.scrollIntoView(true);
- }
- if (fixedTabs) {
- adjustment -= fixedTabs.offsetHeight;
- }
-
- if (fixedDiffStats) {
- adjustment -= fixedDiffStats.offsetHeight;
- }
-
- if (performanceBar) {
- adjustment -= performanceBar.offsetHeight;
- }
-
- if (diffFileHeader) {
- adjustment -= diffFileHeader.offsetHeight;
- }
-
- if (versionMenusContainer) {
- adjustment -= versionMenusContainer.offsetHeight;
- }
+ adjustment -= getElementOffsetHeight(fixedNav);
+ adjustment -= getElementOffsetHeight(fixedTabs);
+ adjustment -= getElementOffsetHeight(fixedDiffStats);
+ adjustment -= getElementOffsetHeight(performanceBar);
+ adjustment -= getElementOffsetHeight(diffFileHeader);
+ adjustment -= getElementOffsetHeight(versionMenusContainer);
if (isInIssuePage()) {
- adjustment -= fixedIssuableTitle.offsetHeight;
+ adjustment -= getElementOffsetHeight(fixedIssuableTitle);
}
if (isInMRPage()) {
adjustment -= topPadding;
}
+ if (target?.scrollIntoView) {
+ target.scrollIntoView(true);
+ }
+
setTimeout(() => {
window.scrollBy(0, adjustment);
});
@@ -172,7 +167,7 @@ export const contentTop = () => {
return size;
},
- () => getOuterHeight('.merge-request-tabs'),
+ () => getOuterHeight('.merge-request-sticky-header, .merge-request-tabs'),
() => getOuterHeight('.js-diff-files-changed'),
() => getOuterHeight('.issue-sticky-header.gl-fixed'),
({ desktop }) => {
@@ -180,7 +175,9 @@ export const contentTop = () => {
let size;
if (desktop && diffsTabIsActive) {
- size = getOuterHeight('.diff-file .file-title-flex-parent:not([style="display:none"])');
+ size = getOuterHeight(
+ '.diffs .diff-file .file-title-flex-parent:not([style="display:none"])',
+ );
}
return 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 6c5d4ecc901..c11cf1a7882 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -271,24 +271,6 @@ export const secondsToMilliseconds = (seconds) => seconds * 1000;
export const secondsToDays = (seconds) => Math.round(seconds / 86400);
/**
- * Converts a numeric utc offset in seconds to +/- hours
- * ie -32400 => -9 hours
- * ie -12600 => -3.5 hours
- *
- * @param {Number} offset UTC offset in seconds as a integer
- *
- * @return {String} the + or - offset in hours
- */
-export const secondsToHours = (offset) => {
- const parsed = parseInt(offset, 10);
- if (Number.isNaN(parsed) || parsed === 0) {
- return `0`;
- }
- const num = offset / 3600;
- return parseInt(num, 10) !== num ? num.toFixed(1) : num;
-};
-
-/**
* Returns the date `n` days after the date provided
*
* @param {Date} date the initial date
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 d07abb72210..737c18d1bce 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -406,3 +406,29 @@ export const durationTimeFormatted = (duration) => {
return `${hh}:${mm}:${ss}`;
};
+
+/**
+ * Converts a numeric utc offset in seconds to +/- hours
+ * ie -32400 => -9 hours
+ * ie -12600 => -3.5 hours
+ *
+ * @param {Number} offset UTC offset in seconds as a integer
+ *
+ * @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`;
+ }
+ const prefix = offset > 0 ? '+' : '-';
+ 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`
+ */
+export const formatTimezone = ({ offset, name }) => `[UTC ${formatUtcOffset(offset)}] ${name}`;
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 48be8af3ff6..3894ec36a0b 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -391,13 +391,15 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
/**
* Indents selected lines to the right by 2 spaces
*
- * @param {Object} textArea - the targeted text area
+ * @param {Object} textArea - jQuery object with the targeted text area
*/
-function indentLines(textArea) {
+function indentLines($textArea) {
+ const textArea = $textArea.get(0);
const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea);
const shiftedLines = [];
let totalAdded = 0;
+ textArea.focus();
textArea.setSelectionRange(startPos, endPos);
lines.forEach((line) => {
@@ -418,13 +420,15 @@ function indentLines(textArea) {
*
* @param {Object} textArea - the targeted text area
*/
-function outdentLines(textArea) {
+function outdentLines($textArea) {
+ const textArea = $textArea.get(0);
const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea);
const shiftedLines = [];
let totalRemoved = 0;
let removedFromFirstline = -1;
let removedFromLine = 0;
+ textArea.focus();
textArea.setSelectionRange(startPos, endPos);
lines.forEach((line) => {
@@ -460,28 +464,10 @@ function outdentLines(textArea) {
);
}
-function handleIndentOutdent(e, textArea) {
- if (e.altKey || e.ctrlKey || e.shiftKey) return;
- if (!e.metaKey) return;
-
- switch (e.key) {
- case ']':
- e.preventDefault();
- indentLines(textArea);
- break;
- case '[':
- e.preventDefault();
- outdentLines(textArea);
- break;
- default:
- break;
- }
-}
-
/* eslint-disable @gitlab/require-i18n-strings */
function handleSurroundSelectedText(e, textArea) {
if (!gon.markdown_surround_selection) return;
- if (e.metaKey) return;
+ if (e.metaKey || e.ctrlKey) return;
if (textArea.selectionStart === textArea.selectionEnd) return;
const keys = {
@@ -532,6 +518,7 @@ function continueOlText(listLineMatch, nextLineMatch) {
}
function handleContinueList(e, textArea) {
+ if (!gon.markdown_automatic_lists) return;
if (!(e.key === 'Enter')) return;
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (textArea.selectionStart !== textArea.selectionEnd) return;
@@ -586,7 +573,6 @@ export function keypressNoteText(e) {
if ($(textArea).atwho?.('isSelecting')) return;
- handleIndentOutdent(e, textArea);
handleContinueList(e, textArea);
handleSurroundSelectedText(e, textArea);
}
@@ -600,15 +586,26 @@ export function compositionEndNoteText() {
}
export function updateTextForToolbarBtn($toolbarBtn) {
- return updateText({
- textArea: $toolbarBtn.closest('.md-area').find('textarea'),
- tag: $toolbarBtn.data('mdTag'),
- cursorOffset: $toolbarBtn.data('mdCursorOffset'),
- blockTag: $toolbarBtn.data('mdBlock'),
- wrap: !$toolbarBtn.data('mdPrepend'),
- select: $toolbarBtn.data('mdSelect'),
- tagContent: $toolbarBtn.attr('data-md-tag-content'),
- });
+ const $textArea = $toolbarBtn.closest('.md-area').find('textarea');
+
+ switch ($toolbarBtn.data('mdCommand')) {
+ case 'indentLines':
+ indentLines($textArea);
+ break;
+ case 'outdentLines':
+ outdentLines($textArea);
+ break;
+ default:
+ return updateText({
+ textArea: $textArea,
+ tag: $toolbarBtn.data('mdTag'),
+ cursorOffset: $toolbarBtn.data('mdCursorOffset'),
+ blockTag: $toolbarBtn.data('mdBlock'),
+ wrap: !$toolbarBtn.data('mdPrepend'),
+ select: $toolbarBtn.data('mdSelect'),
+ tagContent: $toolbarBtn.attr('data-md-tag-content'),
+ });
+ }
}
export function addMarkdownListeners(form) {
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 59645d50e29..367180714df 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,5 +1,5 @@
import { isString, memoize } from 'lodash';
-
+import { base64ToBuffer, bufferToBase64 } from '~/authentication/webauthn/util';
import {
TRUNCATE_WIDTH_DEFAULT_WIDTH,
TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
@@ -513,3 +513,15 @@ export const limitedCounterWithDelimiter = (count) => {
return count > limit ? '1,000+' : count;
};
+
+// Encoding UTF8 ⇢ base64
+export function base64EncodeUnicode(str) {
+ const encoder = new TextEncoder('utf8');
+ return bufferToBase64(encoder.encode(str));
+}
+
+// Decoding base64 ⇢ UTF8
+export function base64DecodeUnicode(str) {
+ const decoder = new TextDecoder('utf8');
+ return decoder.decode(base64ToBuffer(str));
+}
diff --git a/app/assets/javascripts/listbox/index.js b/app/assets/javascripts/listbox/index.js
index f63171e2785..7eacbf7fcdd 100644
--- a/app/assets/javascripts/listbox/index.js
+++ b/app/assets/javascripts/listbox/index.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlListbox } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -31,37 +31,25 @@ export function initListbox(el, { onChange } = {}) {
},
},
render(h) {
- return h(
- GlDropdown,
- {
- props: {
- text: this.text,
- right,
+ return h(GlListbox, {
+ props: {
+ items,
+ right,
+ selected: this.selected,
+ toggleText: this.text,
+ },
+ class: className,
+ on: {
+ select: (selectedValue) => {
+ this.selected = selectedValue;
+ const selectedItem = items.find(({ value }) => value === selectedValue);
+
+ if (typeof onChange === 'function') {
+ onChange(selectedItem);
+ }
},
- class: className,
},
- items.map((item) =>
- h(
- GlDropdownItem,
- {
- props: {
- isCheckItem: true,
- isChecked: this.selected === item.value,
- },
- on: {
- click: () => {
- this.selected = item.value;
-
- if (typeof onChange === 'function') {
- onChange(item);
- }
- },
- },
- },
- item.text,
- ),
- ),
- );
+ });
},
});
}
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index c570f8810a8..ca3f1caec67 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,5 +1,5 @@
export default function initLogoAnimation() {
window.addEventListener('beforeunload', () => {
- document.querySelector('.tanuki-logo').classList.add('animate');
+ document.querySelector('.tanuki-logo')?.classList.add('animate');
});
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c16ed68096d..8e4ebd510aa 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -21,7 +21,7 @@ import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime/timeago_utility';
-import { getLocationHash, visitUrl } from './lib/utils/url_utility';
+import { getLocationHash, visitUrl, mergeUrlParams } from './lib/utils/url_utility';
// everything else
import initFeatureHighlight from './feature_highlight';
@@ -250,11 +250,10 @@ $('form.filter-form').on('submit', function filterFormSubmitCallback(event) {
const link = document.createElement('a');
link.href = this.action;
- const action = `${this.action}${link.search === '' ? '?' : '&'}`;
+ const action = mergeUrlParams(Object.fromEntries(new FormData(this)), this.action);
event.preventDefault();
- // eslint-disable-next-line no-jquery/no-serialize
- visitUrl(`${action}${$(this).serialize()}`);
+ visitUrl(action);
});
const flashContainer = document.querySelector('.flash-container');
diff --git a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
index ce28283ccdf..01f145e0862 100644
--- a/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
+++ b/app/assets/javascripts/members/components/filter_sort/sort_dropdown.vue
@@ -4,6 +4,7 @@ import { mapState } from 'vuex';
import { visitUrl } from '~/lib/utils/url_utility';
import { FIELDS } from '~/members/constants';
import { parseSortParam, buildSortHref } from '~/members/utils';
+import { SORT_DIRECTION_UI } from '~/search/sort/constants';
export default {
name: 'SortDropdown',
@@ -30,6 +31,9 @@ export default {
isAscending() {
return !this.sort.sortDesc;
},
+ sortDirectionData() {
+ return this.isAscending ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
filteredOptions() {
return FIELDS.filter(
(field) => this.tableSortableFields.includes(field.key) && field.sort,
@@ -70,7 +74,7 @@ export default {
data-testid="members-sort-dropdown"
:text="activeOptionLabel"
:is-ascending="isAscending"
- :sort-direction-tool-tip="__('Sort direction')"
+ :sort-direction-tool-tip="sortDirectionData.tooltip"
@sortDirectionChange="handleSortDirectionChange"
>
<gl-sorting-item
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 460dc0041ab..0512bc04085 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -2,7 +2,7 @@
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 { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
+import { canUnban, canOverride, canRemove, 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 {
@@ -90,7 +90,8 @@ export default {
canRemove(member) ||
canResend(member) ||
canUpdate(member, this.currentUserId) ||
- canOverride(member)
+ canOverride(member) ||
+ canUnban(member)
);
},
showField(field) {
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index 0da44b7d468..bf87ab53d36 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -105,9 +105,12 @@ export const buildSortHref = ({
return setUrlParams({ ...filterParams, sort: sortParam }, window.location.href, true);
};
-// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
+// Defined in `ee/app/assets/javascripts/members/utils.js`
export const canOverride = () => false;
+// Defined in `ee/app/assets/javascripts/members/utils.js`
+export const canUnban = () => false;
+
export const parseDataAttributes = (el) => {
const { membersData } = el.dataset;
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
index 7168efa28ad..707e8a0645f 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE } from '../constants';
@@ -75,7 +75,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the file'),
});
});
diff --git a/app/assets/javascripts/merge_conflicts/store/actions.js b/app/assets/javascripts/merge_conflicts/store/actions.js
index 9c101da52f5..f84eaabf9e7 100644
--- a/app/assets/javascripts/merge_conflicts/store/actions.js
+++ b/app/assets/javascripts/merge_conflicts/store/actions.js
@@ -1,5 +1,5 @@
import { setCookie } from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '../constants';
@@ -33,7 +33,7 @@ export const submitResolvedConflicts = async ({ commit, getters }, resolveConfli
window.location.assign(data.redirect_to);
} catch (e) {
commit(types.SET_SUBMIT_STATE, false);
- createFlash({ message: __('Failed to save merge conflicts resolutions. Please try again!') });
+ createAlert({ message: __('Failed to save merge conflicts resolutions. Please try again!') });
}
};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 8cdb9eb5fc4..57b5e9809d2 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, no-underscore-dangle, consistent-return */
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -44,7 +44,7 @@ function MergeRequest(opts) {
}
},
onError: () => {
- createFlash({
+ createAlert({
message: __(
'Someone edited this merge request at the same time you did. Please refresh the page to see changes.',
),
@@ -98,7 +98,7 @@ MergeRequest.prototype.initMRBtnListeners = function () {
MergeRequest.toggleDraftStatus(data.title, wipEvent === 'ready');
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
})
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 0b53a8ede64..17ee2a0d8b6 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,12 +1,12 @@
/* eslint-disable no-new, class-methods-use-this */
import $ from 'jquery';
import Vue from 'vue';
+import { createAlert } from '~/flash';
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import createEventHub from '~/helpers/event_hub_factory';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
-import createFlash from './flash';
import { initDiffStatsDropdown } from './init_diff_stats_dropdown';
import axios from './lib/utils/axios_utils';
@@ -447,7 +447,7 @@ export default class MergeRequestTabs {
.then((m) => m.default())
.catch(() => {
toggleLoader(false);
- createFlash({
+ createAlert({
message: __('An error occurred while fetching this tab.'),
});
});
@@ -480,7 +480,7 @@ export default class MergeRequestTabs {
this.diffsLoaded = true;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred while fetching this tab.'),
});
})
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index f067982fce1..b7629ba001f 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -86,6 +86,7 @@ export default {
<template>
<gl-intersection-observer
+ class="gl-relative gl-top-2"
@appear="setStickyHeaderVisible(false)"
@disappear="setStickyHeaderVisible(true)"
>
diff --git a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
index cac6d722ced..9e537fa2c82 100644
--- a/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/milestones/components/promote_milestone_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
@@ -63,7 +63,7 @@ export default {
visitUrl(response.data.url);
})
.catch((error) => {
- createFlash({
+ createAlert({
message: error,
});
})
diff --git a/app/assets/javascripts/milestones/milestone.js b/app/assets/javascripts/milestones/milestone.js
index 8f2721c2a5b..d9e72340d62 100644
--- a/app/assets/javascripts/milestones/milestone.js
+++ b/app/assets/javascripts/milestones/milestone.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -34,7 +34,7 @@ export default class Milestone {
this.loadedTabs.add(tab);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading milestone tab'),
}),
);
diff --git a/app/assets/javascripts/milestones/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js
index c95ec3dd10b..d4876c3dbe8 100644
--- a/app/assets/javascripts/milestones/milestone_select.js
+++ b/app/assets/javascripts/milestones/milestone_select.js
@@ -121,7 +121,7 @@ export default class MilestoneSelect {
title: __('Started'),
});
}
- if (extraOptions.length) {
+ if (extraOptions.length && data.length) {
extraOptions.push({ type: 'divider' });
}
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index 5bf08be1ead..2995f19c470 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { hide } from '~/tooltips';
@@ -120,7 +120,7 @@ export default class MirrorRepos {
.put(this.mirrorEndpoint, payload)
.then(() => this.removeRow($target))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to remove mirror.'),
}),
);
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index eb7c43034a4..3b7e5a5f2ee 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { escape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -115,7 +115,7 @@ export default class SSHMirror {
const failureMessage = response.data
? response.data.message
: __('An error occurred while detecting host keys');
- createFlash({
+ createAlert({
message: failureMessage,
});
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index e3fcdf716d4..b6ad2d21757 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -11,7 +11,7 @@ import {
import Mousetrap from 'mousetrap';
import VueDraggable from 'vuedraggable';
import { mapActions, mapState, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -176,7 +176,7 @@ export default {
this.setExpandedPanel(expandedPanel);
}
} catch {
- createFlash({
+ createAlert({
message: s__(
'Metrics|Link contains invalid chart information, please verify the link to see the expanded panel.',
),
@@ -201,7 +201,7 @@ export default {
* This watcher is set for future SPA behaviour of the dashboard
*/
if (hasWarnings) {
- createFlash({
+ createAlert({
message: s__(
'Metrics|Your dashboard schema is invalid. Edit the dashboard to correct the YAML schema.',
),
@@ -319,7 +319,7 @@ export default {
this.isRearrangingPanels = isRearrangingPanels;
},
onDateTimePickerInvalid() {
- createFlash({
+ createAlert({
message: s__(
'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.',
),
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 5c99dbc0d98..0ef365c6368 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -134,7 +134,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
if (state.showErrorBanner) {
if (error.response.data && error.response.data.message) {
const { message } = error.response.data;
- createFlash({
+ createAlert({
message: sprintf(
s__('Metrics|There was an error while retrieving metrics. %{message}'),
{ message },
@@ -142,7 +142,7 @@ export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
),
});
} else {
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error while retrieving metrics'),
});
}
@@ -176,7 +176,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
dispatch('fetchDeploymentsData');
if (!state.timeRange) {
- createFlash({
+ createAlert({
message: s__(`Metrics|Invalid time range, please verify.`),
type: 'warning',
});
@@ -207,7 +207,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
});
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__(`Metrics|There was an error while retrieving metrics`),
type: 'warning',
});
@@ -246,7 +246,7 @@ export const fetchPrometheusMetric = (
Sentry.captureException(error);
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error });
- // Continue to throw error so the dashboard can notify using createFlash
+ // Continue to throw error so the dashboard can notify using createAlert
throw error;
});
};
@@ -262,7 +262,7 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
.then((resp) => resp.data)
.then((response) => {
if (!response || !response.deployments) {
- createFlash({
+ createAlert({
message: s__('Metrics|Unexpected deployment data response from prometheus endpoint'),
});
}
@@ -272,7 +272,7 @@ export const fetchDeploymentsData = ({ state, dispatch }) => {
.catch((error) => {
Sentry.captureException(error);
dispatch('receiveDeploymentsDataFailure');
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error getting deployment information.'),
});
});
@@ -302,7 +302,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
)
.then((environments) => {
if (!environments) {
- createFlash({
+ createAlert({
message: s__(
'Metrics|There was an error fetching the environments data, please try again',
),
@@ -314,7 +314,7 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveEnvironmentsDataFailure');
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error getting environments information.'),
});
});
@@ -348,7 +348,7 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
.then(parseAnnotationsResponse)
.then((annotations) => {
if (!annotations) {
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error fetching annotations. Please try again.'),
});
}
@@ -358,7 +358,7 @@ export const fetchAnnotations = ({ state, dispatch, getters }) => {
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveAnnotationsFailure');
- createFlash({
+ createAlert({
message: s__('Metrics|There was an error getting annotations information.'),
});
});
@@ -397,7 +397,7 @@ export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) =
.catch((err) => {
Sentry.captureException(err);
dispatch('receiveDashboardValidationWarningsFailure');
- createFlash({
+ createAlert({
message: s__(
'Metrics|There was an error getting dashboard validation warnings information.',
),
@@ -502,7 +502,7 @@ export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQuery
commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
})
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
s__('Metrics|There was an error getting options for variable "%{name}".'),
{
@@ -569,7 +569,7 @@ export const fetchPanelPreviewMetrics = ({ state, commit }) => {
Sentry.captureException(error);
commit(types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE, { index, error });
- // Continue to throw error so the panel builder can notify using createFlash
+ // Continue to throw error so the panel builder can notify using createAlert
throw error;
});
});
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 297420bf94d..c32a1f4c2ac 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -6,7 +6,6 @@ import initDiffsApp from '../diffs';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import MergeRequest from '../merge_request';
import DiscussionCounter from '../notes/components/discussion_counter.vue';
-import initDiscussionFilters from '../notes/discussion_filters';
import initNotesApp from './init_notes';
export default function initMrNotes() {
@@ -49,7 +48,5 @@ export default function initMrNotes() {
},
});
}
-
- initDiscussionFilters(store);
});
}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index e4a7a7bd9fc..3a67e7925c3 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -5,19 +5,27 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
import discussionNavigator from '../notes/components/discussion_navigator.vue';
import NotesApp from '../notes/components/notes_app.vue';
+import { getNotesFilterData } from '../notes/utils/get_notes_filter_data';
import initWidget from '../vue_merge_request_widget';
export default () => {
+ const el = document.getElementById('js-vue-mr-discussions');
+ if (!el) {
+ return;
+ }
+
+ const notesFilterProps = getNotesFilterData(el);
+
// eslint-disable-next-line no-new
new Vue({
- el: '#js-vue-mr-discussions',
+ el,
name: 'MergeRequestDiscussions',
components: {
NotesApp,
},
store,
data() {
- const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
+ const notesDataset = el.dataset;
const noteableData = JSON.parse(notesDataset.noteableData);
noteableData.noteableType = notesDataset.noteableType;
noteableData.targetType = notesDataset.targetType;
@@ -95,6 +103,7 @@ export default () => {
userData: this.currentUserData,
shouldShow: this.isShowTabActive,
helpPagePath: this.helpPagePath,
+ ...notesFilterProps,
},
}),
]);
diff --git a/app/assets/javascripts/namespaces/leave_by_url.js b/app/assets/javascripts/namespaces/leave_by_url.js
index e00c2abfbef..09757ce17fa 100644
--- a/app/assets/javascripts/namespaces/leave_by_url.js
+++ b/app/assets/javascripts/namespaces/leave_by_url.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { initRails } from '~/lib/utils/rails_ujs';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
@@ -18,7 +18,7 @@ export default function leaveByUrl(namespaceType) {
if (leaveLink) {
leaveLink.click();
} else {
- createFlash({
+ createAlert({
message: sprintf(__('You do not have permission to leave this %{namespaceType}.'), {
namespaceType,
}),
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
index ca6e6567f74..e55bf25a60c 100644
--- a/app/assets/javascripts/nav/components/top_nav_app.vue
+++ b/app/assets/javascripts/nav/components/top_nav_app.vue
@@ -1,5 +1,6 @@
<script>
import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui';
+import Tracking from '~/tracking';
import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
export default {
@@ -19,6 +20,14 @@ export default {
required: true,
},
},
+ methods: {
+ trackToggleEvent() {
+ Tracking.event(undefined, 'click_nav', {
+ label: 'hamburger_menu',
+ property: 'top_navigation',
+ });
+ },
+ },
};
</script>
@@ -32,6 +41,7 @@ export default {
toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
no-flip
no-caret
+ @toggle="trackToggleEvent"
>
<template #button-content>
<gl-icon name="hamburger" />
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index fdcea300388..5437a607e8a 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -44,7 +44,7 @@ export default {
sandbox
:srcdoc="rawCode"
frameborder="0"
- scrolling="no"
+ scrolling="auto"
width="100%"
class="gl-overflow-auto"
></iframe>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index bf35d5c3b25..0d7ff022f8f 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -5,7 +5,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { badgeState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
@@ -111,7 +111,10 @@ export default {
return this.getNoteableData.current_user.can_create_note;
},
canSetInternalNote() {
- return this.getNoteableData.current_user.can_update && (this.isIssue || this.isEpic);
+ return (
+ this.getNoteableData.current_user.can_create_confidential_note &&
+ (this.isIssue || this.isEpic)
+ );
},
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
@@ -276,7 +279,7 @@ export default {
.then(() => badgeState.updateStatus && badgeState.updateStatus())
.then(refreshUserMergeRequestCounts)
.catch(() =>
- createFlash({
+ createAlert({
message: constants.toggleStateErrorMessage[this.noteableType][this.openState],
}),
);
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index 1b1923a90f7..cf6474270a2 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -84,8 +84,8 @@ export default {
return sprintf(text, { commitDisplay, linkStart, linkEnd }, false);
},
- adaptiveAvatarSize() {
- return { default: 24, md: 32 };
+ toggleClass() {
+ return this.discussion.expanded ? 'expanded' : 'collapsed';
},
},
methods: {
@@ -98,16 +98,13 @@ export default {
</script>
<template>
- <div class="discussion-header gl-display-flex gl-align-items-center gl-p-5">
- <div
- v-once
- class="timeline-icon gl-align-self-start gl-flex-shrink-0 gl-flex-shrink gl-mx-3 gl-md-ml-2 gl-md-mr-5"
- >
+ <div class="discussion-header gl-display-flex gl-align-items-center">
+ <div v-once class="timeline-avatar gl-align-self-start gl-flex-shrink-0 gl-flex-shrink">
<gl-avatar-link v-if="author" :href="author.path">
- <gl-avatar :src="author.avatar_url" :alt="author.name" :size="adaptiveAvatarSize" />
+ <gl-avatar :src="author.avatar_url" :alt="author.name" :size="32" />
</gl-avatar-link>
</div>
- <div class="timeline-content w-100">
+ <div class="timeline-content w-100 gl-ml-3" :class="toggleClass">
<note-header
:author="author"
:created-at="firstNote.created_at"
@@ -123,14 +120,14 @@ export default {
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline gl-pl-2"
+ class-name="discussion-headline-light js-discussion-headline gl-pl-3"
/>
<note-edited-text
v-else-if="lastUpdatedAt"
:edited-at="lastUpdatedAt"
:edited-by="lastUpdatedBy"
:action-text="__('Last updated')"
- class-name="discussion-headline-light js-discussion-headline gl-pl-2"
+ class-name="discussion-headline-light js-discussion-headline gl-pl-3"
/>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 6521b86edbb..37935e9c3c6 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -81,16 +81,18 @@ export default {
:class="{
'gl-bg-orange-50': blocksMerge && !allResolved,
'gl-bg-gray-50': !blocksMerge || allResolved,
- 'gl-pr-2': !allResolved,
}"
data-testid="discussions-counter-text"
>
<template v-if="allResolved">
{{ __('All threads resolved!') }}
<gl-dropdown
+ v-gl-tooltip:discussionCounter.hover.bottom
size="small"
category="tertiary"
right
+ :title="__('Thread options')"
+ :aria-label="__('Thread options')"
toggle-class="btn-icon"
class="gl-pt-0! gl-px-2 gl-h-full gl-ml-2"
>
@@ -133,9 +135,12 @@ export default {
@click="jumpNext"
/>
<gl-dropdown
+ v-gl-tooltip:discussionCounter.hover.bottom
size="small"
category="tertiary"
right
+ :title="__('Thread options')"
+ :aria-label="__('Thread options')"
toggle-class="btn-icon"
class="gl-pt-0! gl-px-2"
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
index 8a42fb6bd85..21b48a2a666 100644
--- a/app/assets/javascripts/notes/components/discussion_filter.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -168,7 +168,7 @@ export default {
id="discussion-preferences-dropdown"
class="full-width-mobile"
data-qa-selector="discussion_preferences_dropdown"
- text="Sort or filter"
+ :text="__('Sort or filter')"
:disabled="isLoading"
right
>
diff --git a/app/assets/javascripts/notes/components/discussion_filter_note.vue b/app/assets/javascripts/notes/components/discussion_filter_note.vue
index 61af0b06535..39b3df899a5 100644
--- a/app/assets/javascripts/notes/components/discussion_filter_note.vue
+++ b/app/assets/javascripts/notes/components/discussion_filter_note.vue
@@ -31,7 +31,7 @@ export default {
<div class="timeline-icon d-none d-lg-flex">
<gl-icon name="comment" />
</div>
- <div class="timeline-content">
+ <div class="timeline-content gl-pl-8">
<div data-testid="discussion-filter-timeline-content">
<gl-sprintf :message="$options.i18n.information">
<template #bold="{ content }">
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 6fcfa66ea49..2dbc9b10836 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -142,7 +142,7 @@ export default {
:edited-at="discussion.resolved_at"
:edited-by="discussion.resolved_by"
:action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text gl-mb-2 gl-ml-3"
/>
</template>
<template #avatar-badge>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 9806f8e5dc2..930876e90b1 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui
import { mapActions, mapGetters, mapState } from 'vuex';
import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -238,7 +238,7 @@ export default {
})
.then(() => this.handleAssigneeUpdate(assignees))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong while updating assignees'),
}),
);
@@ -281,6 +281,7 @@ export default {
>
{{ __('Contributor') }}
</user-access-role-badge>
+ <span class="note-actions__mobile-spacer"></span>
<gl-button
v-if="canResolve"
ref="resolveButton"
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 835750cc137..9d59994788e 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import AwardsList from '~/vue_shared/components/awards_list.vue';
@@ -49,7 +49,7 @@ export default {
};
this.toggleAwardRequest(data).catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
}),
);
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index f700802d6bc..f3530344181 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -9,8 +9,6 @@ import {
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
@@ -21,13 +19,11 @@ export default {
GlIcon,
GlBadge,
GlLoadingIcon,
- UserNameWithStatus,
},
directives: {
SafeHtml,
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
author: {
type: Object,
@@ -74,12 +70,15 @@ export default {
required: false,
default: false,
},
+ isSystemNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
isUsernameLinkHovered: false,
- emojiTitle: '',
- authorStatusHasTooltip: false,
};
},
computed: {
@@ -100,15 +99,6 @@ export default {
'js-user-link': true,
};
},
- authorStatus() {
- if (this.author?.show_status) {
- return this.author.status_tooltip_html;
- }
- return false;
- },
- emojiElement() {
- return this.$refs?.authorStatus?.querySelector('gl-emoji');
- },
authorName() {
return this.author.name;
},
@@ -116,14 +106,6 @@ export default {
return s__('Notes|This internal note will always remain confidential');
},
},
- mounted() {
- this.emojiTitle = this.emojiElement ? this.emojiElement.getAttribute('title') : '';
-
- const authorStatusTitle = this.$refs?.authorStatus
- ?.querySelector('.user-status-emoji')
- ?.getAttribute('title');
- this.authorStatusHasTooltip = authorStatusTitle && authorStatusTitle !== '';
- },
methods: {
...mapActions(['setTargetNoteHash']),
handleToggle() {
@@ -134,12 +116,6 @@ export default {
this.setTargetNoteHash(this.noteTimestampLink);
}
},
- removeEmojiTitle() {
- this.emojiElement.removeAttribute('title');
- },
- addEmojiTitle() {
- this.emojiElement.setAttribute('title', this.emojiTitle);
- },
handleUsernameMouseEnter() {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseenter'));
this.isUsernameLinkHovered = true;
@@ -148,9 +124,6 @@ export default {
this.$refs.authorNameLink.dispatchEvent(new Event('mouseleave'));
this.isUsernameLinkHovered = false;
},
- userAvailability(selectedAuthor) {
- return selectedAuthor?.availability || '';
- },
},
i18n: {
showThread: __('Show thread'),
@@ -185,35 +158,11 @@ export default {
:data-user-id="author.id"
:data-username="author.username"
>
- <span
- v-if="glFeatures.removeUserAttributesProjects || glFeatures.removeUserAttributesGroups"
- class="note-header-author-name gl-font-weight-bold"
- >
+ <span class="note-header-author-name gl-font-weight-bold">
{{ authorName }}
</span>
- <user-name-with-status
- v-else
- :name="authorName"
- :availability="userAvailability(author)"
- container-classes="note-header-author-name gl-font-weight-bold"
- />
</a>
- <span
- v-if="
- authorStatus &&
- !glFeatures.removeUserAttributesProjects &&
- !glFeatures.removeUserAttributesGroups
- "
- ref="authorStatus"
- v-safe-html:[$options.safeHtmlConfig]="authorStatus"
- v-on="
- authorStatusHasTooltip ? { mouseenter: removeEmojiTitle, mouseleave: addEmojiTitle } : {}
- "
- ></span>
- <span
- v-if="!glFeatures.removeUserAttributesProjects && !glFeatures.removeUserAttributesGroups"
- class="text-nowrap author-username"
- >
+ <span v-if="!isSystemNote" class="text-nowrap author-username">
<a
ref="authorUsernameLink"
class="author-username-link"
@@ -252,7 +201,7 @@ export default {
data-testid="internalNoteIndicator"
variant="warning"
size="sm"
- class="gl-mb-3 gl-ml-2"
+ class="gl-ml-2"
:title="internalNoteTooltip"
>
{{ __('Internal note') }}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index afa5e39d8b0..50d166b6db5 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -2,7 +2,7 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -247,7 +247,7 @@ export default {
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
);
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index e51969f95c7..c4b3111b919 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -5,7 +5,7 @@ import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -199,9 +199,6 @@ export default {
isMRDiffView() {
return this.line && !this.isOverviewTab;
},
- authorAvatarAdaptiveSize() {
- return { default: 24, md: 32 };
- },
},
created() {
const line = this.note.position?.line_range?.start || this.line;
@@ -273,7 +270,7 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting your note. Please try again.'),
});
this.isDeleting = false;
@@ -352,7 +349,7 @@ export default {
},
handleUpdateError() {
const msg = __('Something went wrong while editing your comment. Please try again.');
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
@@ -409,13 +406,13 @@ export default {
:class="{ ...classNameBindings, 'internal-note': note.internal }"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note note-wrapper"
+ class="note note-wrapper note-comment"
data-qa-selector="noteable_note_container"
>
<div
v-if="showMultiLineComment"
data-testid="multiline-comment"
- class="gl-mb-5 gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-pb-4"
+ class="gl-text-gray-500 gl-border-gray-100 gl-border-b-solid gl-border-b-1 gl-px-5 gl-py-3"
>
<gl-sprintf :message="__('Comment on lines %{startLine} to %{endLine}')">
<template #startLine>
@@ -427,7 +424,7 @@ export default {
</gl-sprintf>
</div>
- <div v-if="isMRDiffView" class="gl-float-left gl-mt-n1 gl-mr-3">
+ <div v-if="isMRDiffView" class="timeline-avatar gl-float-left gl-pt-2">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
@@ -440,13 +437,13 @@ export default {
</gl-avatar-link>
</div>
- <div v-else class="gl-float-left gl-pl-3 gl-md-pl-2">
+ <div v-else class="timeline-avatar gl-float-left">
<gl-avatar-link :href="author.path">
<gl-avatar
:src="author.avatar_url"
:entity-name="author.username"
:alt="author.name"
- :size="authorAvatarAdaptiveSize"
+ :size="32"
/>
<slot name="avatar-badge"></slot>
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
new file mode 100644
index 00000000000..e4f88962731
--- /dev/null
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -0,0 +1,38 @@
+<script>
+import DiscussionFilter from './discussion_filter.vue';
+
+export default {
+ components: {
+ TimelineToggle: () => import('./timeline_toggle.vue'),
+ DiscussionFilter,
+ },
+ inject: {
+ showTimelineViewToggle: {
+ default: false,
+ },
+ },
+ props: {
+ notesFilters: {
+ type: Array,
+ required: true,
+ },
+ notesFilterValue: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-sm-align-items-center gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-pt-5 gl-mt-5 gl-border-t"
+ >
+ <h2 class="gl-font-size-h1 gl-m-0">{{ __('Activity') }}</h2>
+ <div class="gl-display-flex gl-gap-3 gl-w-full gl-sm-w-auto gl-mt-3 gl-sm-mt-0">
+ <timeline-toggle v-if="showTimelineViewToggle" />
+ <discussion-filter :filters="notesFilters" :selected-value="notesFilterValue" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 37bc8bad305..9c2ff2c3e7f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,7 +1,7 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@@ -19,10 +19,12 @@ import DiscussionFilterNote from './discussion_filter_note.vue';
import NoteableDiscussion from './noteable_discussion.vue';
import NoteableNote from './noteable_note.vue';
import SidebarSubscription from './sidebar_subscription.vue';
+import NotesActivityHeader from './notes_activity_header.vue';
export default {
name: 'NotesApp',
components: {
+ NotesActivityHeader,
NoteableNote,
NoteableDiscussion,
SystemNote,
@@ -46,6 +48,15 @@ export default {
type: Object,
required: true,
},
+ notesFilters: {
+ type: Array,
+ required: true,
+ },
+ notesFilterValue: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
userData: {
type: Object,
required: false,
@@ -221,7 +232,7 @@ export default {
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching comments. Please try again.'),
});
});
@@ -281,6 +292,7 @@ export default {
<template>
<div v-show="shouldShow" id="notes">
<sidebar-subscription :iid="noteableData.iid" :noteable-data="noteableData" />
+ <notes-activity-header :notes-filters="notesFilters" :notes-filter-value="notesFilterValue" />
<ordered-layout :slot-keys="slotKeys">
<template #form>
<comment-form
@@ -292,7 +304,11 @@ export default {
<template #comments>
<ul id="notes-list" class="notes main-notes-list timeline">
<template v-for="discussion in allDiscussions">
- <skeleton-loading-container v-if="discussion.isSkeletonNote" :key="discussion.id" />
+ <skeleton-loading-container
+ v-if="discussion.isSkeletonNote"
+ :key="discussion.id"
+ class="note-skeleton"
+ />
<timeline-entry-item v-else-if="discussion.isDraft" :key="discussion.id">
<draft-note :draft="discussion" />
</timeline-entry-item>
@@ -327,7 +343,7 @@ export default {
:help-page-path="helpPagePath"
/>
</template>
- <discussion-filter-note v-show="commentsDisabled" />
+ <discussion-filter-note v-if="commentsDisabled" />
</ul>
</template>
</ordered-layout>
diff --git a/app/assets/javascripts/notes/components/timeline_toggle.vue b/app/assets/javascripts/notes/components/timeline_toggle.vue
index 8632eea5d8e..59a3cc2d306 100644
--- a/app/assets/javascripts/notes/components/timeline_toggle.vue
+++ b/app/assets/javascripts/notes/components/timeline_toggle.vue
@@ -53,6 +53,7 @@ export default {
:selected="timelineEnabled"
:title="tooltip"
:aria-label="tooltip"
+ data-testid="timeline-toggle-button"
@click="toggleTimeline"
/>
</template>
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
index 2bd3488ae1b..734e08dd586 100644
--- a/app/assets/javascripts/notes/components/toggle_replies_widget.vue
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -61,7 +61,7 @@ export default {
<template>
<li
:class="liClasses"
- class="gl-display-flex! gl-align-items-center gl-flex-wrap gl-bg-gray-10 gl-py-3 gl-px-5 gl-border-t"
+ class="toggle-replies-widget gl-display-flex! gl-align-items-center gl-flex-wrap gl-bg-gray-10 gl-py-3 gl-px-5 gl-border"
>
<gl-button
ref="toggle"
@@ -75,7 +75,7 @@ export default {
<user-avatar-link
v-for="author in uniqueAuthors"
:key="author.username"
- class="gl-mr-3"
+ class="gl-mr-3 reply-author-avatar"
:link-href="author.path"
:img-alt="author.name"
img-css-classes="gl-mr-0!"
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
deleted file mode 100644
index 104e9d4183a..00000000000
--- a/app/assets/javascripts/notes/discussion_filters.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import Vue from 'vue';
-import DiscussionFilter from './components/discussion_filter.vue';
-
-export default (store) => {
- const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
-
- if (discussionFilterEl) {
- const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
- const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
- const filters = Object.keys(filterValues).map((entry) => ({
- title: entry,
- value: filterValues[entry],
- }));
- const props = { filters };
-
- if (defaultFilter) {
- props.selectedValue = parseInt(defaultFilter, 10);
- }
-
- return new Vue({
- el: discussionFilterEl,
- name: 'DiscussionFilterRoot',
- components: {
- DiscussionFilter,
- },
- store,
- render(createElement) {
- return createElement('discussion-filter', { props });
- },
- });
- }
-
- return null;
-};
diff --git a/app/assets/javascripts/notes/i18n.js b/app/assets/javascripts/notes/i18n.js
index 08792fd1a3f..9b5fd69f816 100644
--- a/app/assets/javascripts/notes/i18n.js
+++ b/app/assets/javascripts/notes/i18n.js
@@ -16,7 +16,7 @@ export const COMMENT_FORM = {
bodyPlaceholderInternal: __('Write an internal note or drag your files here…'),
internal: s__('Notes|Make this an internal note'),
internalVisibility: s__(
- 'Notes|Internal notes are only visible to the author, assignees, and members with the role of Reporter or higher',
+ 'Notes|Internal notes are only visible to members with the role of Reporter or higher',
),
discussionThatNeedsResolution: __(
'Discuss a specific suggestion or question that needs to be resolved.',
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 054a5bd36e2..defcb0533b7 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,9 +1,8 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import NotesApp from './components/notes_app.vue';
-import initDiscussionFilters from './discussion_filters';
import { store } from './stores';
-import initTimelineToggle from './timeline';
+import { getNotesFilterData } from './utils/get_notes_filter_data';
export default () => {
const el = document.getElementById('js-vue-notes');
@@ -11,6 +10,9 @@ export default () => {
return;
}
+ const notesFilterProps = getNotesFilterData(el);
+ const showTimelineViewToggle = parseBoolean(el.dataset.showTimelineViewToggle);
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -19,6 +21,9 @@ export default () => {
NotesApp,
},
store,
+ provide: {
+ showTimelineViewToggle,
+ },
data() {
const notesDataset = el.dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
@@ -56,11 +61,9 @@ export default () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ ...notesFilterProps,
},
});
},
});
-
- initDiscussionFilters(store);
- initTimelineToggle(store);
};
diff --git a/app/assets/javascripts/notes/mixins/diff_line_note_form.js b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
index 7b9c0959464..9a140029c07 100644
--- a/app/assets/javascripts/notes/mixins/diff_line_note_form.js
+++ b/app/assets/javascripts/notes/mixins/diff_line_note_form.js
@@ -1,7 +1,7 @@
import { mapActions, mapGetters, mapState } from 'vuex';
import { getDraftReplyFormData, getDraftFormData } from '~/batch_comments/utils';
import { TEXT_DIFF_POSITION_TYPE, IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { clearDraft } from '~/lib/utils/autosave';
import { s__ } from '~/locale';
import { formatLineRange } from '~/notes/components/multiline_comment_utils';
@@ -42,7 +42,7 @@ export default {
this.handleClearForm(this.discussion.line_code);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
@@ -82,7 +82,7 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('MergeRequests|An error occurred while saving the draft comment.'),
});
});
diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js
index db5f9ebf3f0..d75a4158440 100644
--- a/app/assets/javascripts/notes/mixins/discussion_navigation.js
+++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js
@@ -1,136 +1,12 @@
import { mapGetters, mapActions, mapState } from 'vuex';
-import { scrollToElementWithContext, scrollToElement, contentTop } from '~/lib/utils/common_utils';
-import { updateHistory } from '~/lib/utils/url_utility';
-import eventHub from '../event_hub';
-
-/**
- * @param {string} selector
- * @returns {boolean}
- */
-function scrollTo(selector, { withoutContext = false, offset = 0 } = {}) {
- const el = document.querySelector(selector);
- const scrollFunction = withoutContext ? scrollToElement : scrollToElementWithContext;
-
- if (el) {
- scrollFunction(el, {
- behavior: 'auto',
- offset,
- });
- return true;
- }
-
- return false;
-}
-
-function updateUrlWithNoteId(noteId) {
- const newHistoryEntry = {
- state: null,
- title: window.title,
- url: `#note_${noteId}`,
- replace: true,
- };
-
- if (noteId) {
- // Temporarily mask the ID to avoid the browser default
- // scrolling taking over which is broken with virtual
- // scrolling enabled.
- const note = document.querySelector(`#note_${noteId}`);
- note?.setAttribute('id', `masked::${note.id}`);
-
- // Update the hash now that the ID "doesn't exist" in the page
- updateHistory(newHistoryEntry);
-
- // Unmask the note's ID
- note?.setAttribute('id', `note_${noteId}`);
- }
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- */
-function diffsJump({ expandDiscussion }, id, firstNoteId) {
- const selector = `ul.notes[data-discussion-id="${id}"]`;
-
- eventHub.$once('scrollToDiscussion', () => {
- scrollTo(selector);
- // Wait for the discussion scroll before updating to the more specific ID
- setTimeout(() => updateUrlWithNoteId(firstNoteId), 0);
- });
- expandDiscussion({ discussionId: id });
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- * @returns {boolean}
- */
-function discussionJump({ expandDiscussion }, id) {
- const selector = `div.discussion[data-discussion-id="${id}"]`;
- expandDiscussion({ discussionId: id });
- return scrollTo(selector, {
- withoutContext: true,
- offset: window.gon?.features?.movedMrSidebar ? -28 : 0,
- });
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {string} id Discussion id we are jumping to
- */
-function switchToDiscussionsTabAndJumpTo(self, id) {
- window.mrTabs.eventHub.$once('MergeRequestTabChange', () => {
- setTimeout(() => discussionJump(self, id), 0);
- });
-
- window.mrTabs.tabShown('show');
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {object} discussion Discussion we are jumping to
- */
-function jumpToDiscussion(self, discussion) {
- const { id, diff_discussion: isDiffDiscussion, notes } = discussion;
- const firstNoteId = notes?.[0]?.id;
- if (id) {
- const activeTab = window.mrTabs.currentAction;
-
- if (activeTab === 'diffs' && isDiffDiscussion) {
- diffsJump(self, id, firstNoteId);
- } else {
- switchToDiscussionsTabAndJumpTo(self, id);
- }
- }
-}
-
-/**
- * @param {object} self Component instance with mixin applied
- * @param {function} fn Which function used to get the target discussion's id
- */
-function handleDiscussionJump(self, fn) {
- const isDiffView = window.mrTabs.currentAction === 'diffs';
- const targetId = fn(self.currentDiscussionId, isDiffView);
- const discussion = self.getDiscussion(targetId);
- const discussionFilePath = discussion?.diff_file?.file_path;
-
- window.location.hash = '';
-
- if (discussionFilePath) {
- self.scrollToFile({
- path: discussionFilePath,
- });
- }
-
- self.$nextTick(() => {
- jumpToDiscussion(self, discussion);
- self.setCurrentDiscussionId(targetId);
- });
-}
+import { scrollToElement, contentTop } from '~/lib/utils/common_utils';
function getAllDiscussionElements() {
+ const containerEl = window.mrTabs?.currentAction === 'diffs' ? '.diffs' : '.notes';
return Array.from(
- document.querySelectorAll('[data-discussion-id]:not([data-discussion-resolved])'),
+ document.querySelectorAll(
+ `${containerEl} div[data-discussion-id]:not([data-discussion-resolved])`,
+ ),
);
}
@@ -182,14 +58,10 @@ function getPreviousDiscussion() {
}
function handleJumpForBothPages(getDiscussion, ctx, fn, scrollOptions) {
- if (window.mrTabs.currentAction !== 'show') {
- handleDiscussionJump(ctx, fn);
- } else {
- const discussion = getDiscussion();
- const id = discussion.dataset.discussionId;
- ctx.expandDiscussion({ discussionId: id });
- scrollToElement(discussion, scrollOptions);
- }
+ const discussion = getDiscussion();
+ const id = discussion.dataset.discussionId;
+ ctx.expandDiscussion({ discussionId: id });
+ scrollToElement(discussion, scrollOptions);
}
export default {
@@ -205,9 +77,11 @@ export default {
},
methods: {
...mapActions(['expandDiscussion', 'setCurrentDiscussionId']),
- ...mapActions('diffs', ['scrollToFile']),
+ ...mapActions('diffs', ['scrollToFile', 'disableVirtualScroller']),
+
+ async jumpToNextDiscussion(scrollOptions) {
+ await this.disableVirtualScroller();
- jumpToNextDiscussion(scrollOptions) {
handleJumpForBothPages(
getNextDiscussion,
this,
@@ -216,7 +90,9 @@ export default {
);
},
- jumpToPreviousDiscussion(scrollOptions) {
+ async jumpToPreviousDiscussion(scrollOptions) {
+ await this.disableVirtualScroller();
+
handleJumpForBothPages(
getPreviousDiscussion,
this,
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 9783def1b46..44751020173 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
export default {
@@ -46,7 +46,7 @@ export default {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
- createFlash({
+ createAlert({
message: msg,
parent: this.$el,
});
diff --git a/app/assets/javascripts/notes/timeline.js b/app/assets/javascripts/notes/timeline.js
deleted file mode 100644
index df6d1b21400..00000000000
--- a/app/assets/javascripts/notes/timeline.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import TimelineToggle from './components/timeline_toggle.vue';
-
-export default function initTimelineToggle(store) {
- const el = document.getElementById('js-incidents-timeline-toggle');
-
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- render(createElement) {
- return createElement(TimelineToggle);
- },
- });
-}
diff --git a/app/assets/javascripts/notes/utils/get_notes_filter_data.js b/app/assets/javascripts/notes/utils/get_notes_filter_data.js
new file mode 100644
index 00000000000..6d62ab5e91b
--- /dev/null
+++ b/app/assets/javascripts/notes/utils/get_notes_filter_data.js
@@ -0,0 +1,21 @@
+/**
+ * Returns parsed notes filter data from a given element's dataset
+ *
+ * @param {Element} el containing info in the dataset
+ */
+export const getNotesFilterData = (el) => {
+ const { notesFilterValue: valueData, notesFilters: filtersData } = el.dataset;
+
+ const filtersParsed = filtersData ? JSON.parse(filtersData) : {};
+ const filters = Object.keys(filtersParsed).map((key) => ({
+ title: key,
+ value: filtersParsed[key],
+ }));
+
+ const value = valueData ? Number(valueData) : undefined;
+
+ return {
+ notesFilters: filters,
+ notesFilterValue: value,
+ };
+};
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
index 529eb7d207b..5f60cab8bdd 100644
--- a/app/assets/javascripts/operation_settings/store/actions.js
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -35,7 +35,7 @@ export const receiveSaveChangesError = (_, error) => {
const { response = {} } = error;
const message = response.data && response.data.message ? response.data.message : '';
- createFlash({
+ createAlert({
message: `${__('There was an error saving your changes.')} ${message}`,
});
};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 9e8eb92d87a..597df2b9bc3 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { n__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
@@ -69,7 +69,7 @@ export default {
return this.queryVariables;
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index 9ebbdfa920d..8b66165a57a 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -1,7 +1,7 @@
<script>
import { GlResizeObserverDirective, GlEmptyState } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
@@ -66,7 +66,7 @@ export default {
this.updateBreadcrumb();
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index c1bd71de646..794be8d5195 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
@@ -100,7 +100,7 @@ export default {
this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
additionalDetails: {
@@ -115,7 +115,7 @@ export default {
return data[this.graphqlResource]?.containerRepositories.nodes;
},
error() {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
index 26d4aa13715..223f427ce0e 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import {
DELETE_PACKAGE_ERROR_MESSAGE,
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
@@ -20,7 +20,7 @@ export const fetchPackageVersions = ({ commit, state }) => {
}
})
.catch(() => {
- createFlash({ message: FETCH_PACKAGE_VERSIONS_ERROR, type: 'warning' });
+ createAlert({ message: FETCH_PACKAGE_VERSIONS_ERROR, variant: VARIANT_WARNING });
})
.finally(() => {
commit(types.SET_LOADING, false);
@@ -33,7 +33,7 @@ export const deletePackage = ({
},
}) => {
return Api.deleteProjectPackage(project_id, id).catch(() => {
- createFlash({ message: DELETE_PACKAGE_ERROR_MESSAGE, type: 'warning' });
+ createAlert({ message: DELETE_PACKAGE_ERROR_MESSAGE, variant: VARIANT_WARNING });
});
};
@@ -51,9 +51,9 @@ export const deletePackageFile = (
.then(() => {
const filtered = packageFiles.filter((f) => f.id !== fileId);
commit(types.UPDATE_PACKAGE_FILES, filtered);
- createFlash({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, type: 'success' });
+ createAlert({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, variant: VARIANT_SUCCESS });
})
.catch(() => {
- createFlash({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, type: 'warning' });
+ createAlert({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, variant: VARIANT_WARNING });
});
};
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
index dab4a051d0c..8b6a5c59847 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
@@ -81,10 +81,9 @@ export default {
},
},
i18n: {
- deleteModalContent: s__(
- 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
- ),
- modalAction: s__('PackageRegistry|Delete package'),
+ deleteModalContent: s__('PackageRegistry|You are about to delete %{name}, are you sure?'),
+ modalTitle: s__('PackageRegistry|Delete package'),
+ modalAction: s__('PackageRegistry|Permanently delete'),
},
};
</script>
@@ -120,13 +119,13 @@ export default {
<gl-modal
ref="packageListDeleteModal"
size="sm"
- modal-id="confirm-delete-pacakge"
+ modal-id="confirm-delete-package"
:action-primary="deleteModalActionPrimaryProps"
:action-cancel="deleteModalActionCancelProps"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ $options.i18n.modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalTitle }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 184a24047eb..2adf6187c4b 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import {
@@ -84,7 +84,7 @@ export default {
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
- createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
+ createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
index 51a38c434cb..37b51797490 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
import {
@@ -43,7 +43,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
dispatch('receivePackagesListSuccess', { data, headers });
})
.catch(() => {
- createFlash({
+ createAlert({
message: FETCH_PACKAGES_LIST_ERROR_MESSAGE,
});
})
@@ -54,7 +54,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
if (!_links || !_links.delete_api_path) {
- createFlash({
+ createAlert({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
const error = new Error(MISSING_DELETE_PATH_ERROR);
@@ -69,14 +69,14 @@ export const requestDeletePackage = ({ dispatch, state }, { _links }) => {
const page = getNewPaginationPage(currentPage, perPage, total - 1);
dispatch('requestPackagesList', { page });
- createFlash({
+ createAlert({
message: DELETE_PACKAGE_SUCCESS_MESSAGE,
- type: 'success',
+ variant: VARIANT_SUCCESS,
});
})
.catch(() => {
dispatch('setLoading', false);
- createFlash({
+ createAlert({
message: DELETE_PACKAGE_ERROR_MESSAGE,
});
});
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
index 11fd0db3106..cee976656f9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue
@@ -2,7 +2,8 @@
import { GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
-import { __ } from '~/locale';
+import { __, s__, sprintf } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime_utility';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils';
@@ -25,6 +26,7 @@ export default {
},
inject: ['isGroupPage'],
i18n: {
+ lastDownloadedAt: s__('PackageRegistry|Last downloaded %{dateTime}'),
packageInfo: __('v%{version} published %{timeAgo}'),
},
props: {
@@ -39,6 +41,11 @@ export default {
};
},
computed: {
+ packageLastDownloadedAtDisplay() {
+ return sprintf(this.$options.i18n.lastDownloadedAt, {
+ dateTime: formatDate(this.packageEntity.lastDownloadedAt, 'mmm d, yyyy'),
+ });
+ },
packageTypeDisplay() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
@@ -136,6 +143,15 @@ export default {
<metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
</template>
+ <template v-if="packageEntity.lastDownloadedAt" #metadata-last-downloaded-at>
+ <metadata-item
+ data-testid="package-last-downloaded-at"
+ icon="download"
+ :text="packageLastDownloadedAtDisplay"
+ size="m"
+ />
+ </template>
+
<template #right-actions>
<slot name="delete-button"></slot>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
index 7a85fd3052e..e1cf4883029 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_package.vue
@@ -1,6 +1,6 @@
<script>
import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { s__ } from '~/locale';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/package_registry/constants';
@@ -39,15 +39,15 @@ export default {
throw data.destroyPackage.errors[0];
}
if (this.showSuccessAlert) {
- createFlash({
+ createAlert({
message: this.$options.i18n.successMessage,
- type: 'success',
+ variant: VARIANT_SUCCESS,
});
}
} catch (error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.errorMessage,
- type: 'warning',
+ variant: VARIANT_WARNING,
captureError: true,
error,
});
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 e84f181e9b2..c6583b8f09f 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
@@ -122,10 +122,9 @@ export default {
},
},
i18n: {
- deleteModalContent: s__(
- 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
- ),
- modalAction: s__('PackageRegistry|Delete package'),
+ deleteModalContent: s__('PackageRegistry|You are about to delete %{name}, are you sure?'),
+ modalTitle: s__('PackageRegistry|Delete package'),
+ modalAction: s__('PackageRegistry|Permanently delete'),
errorMessageBodyAlert: s__(
'PackageRegistry|There was a timeout and the package was not published. Delete this package and try again.',
),
@@ -172,14 +171,14 @@ export default {
<gl-modal
v-model="showDeleteModal"
- modal-id="confirm-delete-pacakge"
+ modal-id="confirm-delete-package"
size="sm"
:action-primary="deleteModalActionPrimaryProps"
:action-cancel="deleteModalActionCancelProps"
@ok="deleteItemConfirmation"
@cancel="deleteItemCanceled"
>
- <template #modal-title>{{ $options.i18n.modalAction }}</template>
+ <template #modal-title>{{ $options.i18n.modalTitle }}</template>
<gl-sprintf :message="$options.i18n.deleteModalContent">
<template #name>
<strong>{{ deletePackageName }}</strong>
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 06a04ee248a..4e35176c757 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -78,6 +78,17 @@ export const TRACKING_ACTION_CLICK_COMMIT_LINK = 'click_commit_link_from_package
export const TRACKING_LABEL_PACKAGE_HISTORY = 'package_history';
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
+
+export const DELETE_MODAL_TITLE = s__('PackageRegistry|Delete package version');
+export const DELETE_MODAL_CONTENT = s__(
+ `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
+);
+export const DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT = s__(
+ `PackageRegistry|Deleting all package assets will remove version %{version} of %{name}. Are you sure?`,
+);
+export const DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT = s__(
+ `PackageRegistry|Deleting the last package asset will remove version %{version} of %{name}. Are you sure?`,
+);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package asset.',
);
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index f3f0d096d10..8e50c95b10b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -4,6 +4,7 @@ query getPackageDetails($id: PackagesPackageID!) {
name
packageType
version
+ lastDownloadedAt
createdAt
updatedAt
status
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index c10fc914d56..eeed56b77c3 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -10,7 +10,7 @@ import {
GlTabs,
GlSprintf,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
@@ -44,6 +44,10 @@ import {
DELETE_PACKAGE_FILES_ERROR_MESSAGE,
DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
+ DELETE_MODAL_TITLE,
+ DELETE_MODAL_CONTENT,
+ DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
+ DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
} from '~/packages_and_registries/package_registry/constants';
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
@@ -86,6 +90,7 @@ export default {
},
data() {
return {
+ deletePackageModalContent: DELETE_MODAL_CONTENT,
filesToDelete: [],
mutationLoading: false,
packageEntity: {},
@@ -101,7 +106,7 @@ export default {
return data.package || {};
},
error(error) {
- createFlash({
+ createAlert({
message: FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
captureError: true,
error,
@@ -205,20 +210,18 @@ export default {
if (data?.destroyPackageFiles?.errors[0]) {
throw data.destroyPackageFiles.errors[0];
}
- createFlash({
- message:
- ids.length === 1
- ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
- : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
- type: 'success',
+ createAlert({
+ message: this.isLastItem(ids)
+ ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE
+ : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
+ variant: VARIANT_SUCCESS,
});
} catch (error) {
- createFlash({
- message:
- ids.length === 1
- ? DELETE_PACKAGE_FILE_ERROR_MESSAGE
- : DELETE_PACKAGE_FILES_ERROR_MESSAGE,
- type: 'warning',
+ createAlert({
+ message: this.isLastItem(ids)
+ ? DELETE_PACKAGE_FILE_ERROR_MESSAGE
+ : DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ variant: VARIANT_WARNING,
captureError: true,
error,
});
@@ -231,18 +234,26 @@ export default {
files.length === this.packageFiles.length &&
!this.packageEntity.packageFiles?.pageInfo?.hasNextPage
) {
+ if (this.isLastItem(files)) {
+ this.deletePackageModalContent = DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT;
+ } else {
+ this.deletePackageModalContent = DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT;
+ }
this.$refs.deleteModal.show();
} else {
this.filesToDelete = files;
- if (files.length === 1) {
+ if (this.isLastItem(files)) {
this.$refs.deleteFileModal.show();
} else if (files.length > 1) {
this.$refs.deleteFilesModal.show();
}
}
},
+ isLastItem(items) {
+ return items.length === 1;
+ },
confirmFilesDelete() {
- if (this.filesToDelete.length === 1) {
+ if (this.isLastItem(this.filesToDelete)) {
this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION);
} else {
this.track(DELETE_PACKAGE_FILES_TRACKING_ACTION);
@@ -250,12 +261,12 @@ export default {
this.deletePackageFiles(this.filesToDelete.map((file) => file.id));
this.filesToDelete = [];
},
+ resetDeleteModalContent() {
+ this.deletePackageModalContent = DELETE_MODAL_CONTENT;
+ },
},
i18n: {
- deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
- deleteModalContent: s__(
- `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
- ),
+ DELETE_MODAL_TITLE,
deleteFileModalTitle: s__(`PackageRegistry|Delete package asset`),
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
@@ -263,7 +274,7 @@ export default {
},
modal: {
packageDeletePrimaryAction: {
- text: __('Delete'),
+ text: s__('PackageRegistry|Permanently delete'),
attributes: [
{ variant: 'danger' },
{ category: 'primary' },
@@ -371,10 +382,11 @@ export default {
:action-primary="$options.modal.packageDeletePrimaryAction"
:action-cancel="$options.modal.cancelAction"
@primary="deletePackage(packageEntity)"
+ @hidden="resetDeleteModalContent"
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
>
- <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
- <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #modal-title>{{ $options.i18n.DELETE_MODAL_TITLE }}</template>
+ <gl-sprintf :message="deletePackageModalContent">
<template #version>
<strong>{{ packageEntity.version }}</strong>
</template>
@@ -398,7 +410,7 @@ export default {
@canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
>
<template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template>
- <gl-sprintf v-if="filesToDelete.length === 1" :message="$options.i18n.deleteFileModalContent">
+ <gl-sprintf v-if="isLastItem(filesToDelete)" :message="$options.i18n.deleteFileModalContent">
<template #filename>
<strong>{{ filesToDelete[0].fileName }}</strong>
</template>
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 38df701157a..ed9ab0367dd 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,6 +1,6 @@
<script>
import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
@@ -105,7 +105,7 @@ export default {
const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT);
if (showAlert) {
// to be refactored to use gl-alert
- createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' });
+ createAlert({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, variant: VARIANT_INFO });
const cleanUrl = window.location.href.split('?')[0];
historyReplaceState(cleanUrl);
}
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
index 72e68aca070..b8405b09840 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
@@ -57,6 +57,9 @@ export default {
isEnabled() {
return this.containerExpirationPolicy || this.enableHistoricEntries;
},
+ isLoading() {
+ return this.$apollo.queries.containerExpirationPolicy.loading;
+ },
showDisabledFormMessage() {
return !this.isEnabled && !this.fetchSettingsError;
},
@@ -86,10 +89,10 @@ export default {
<container-expiration-policy-form
v-if="isEnabled"
v-model="workingCopy"
- :is-loading="$apollo.queries.containerExpirationPolicy.loading"
+ :is-loading="isLoading"
:is-edited="isEdited"
/>
- <template v-else>
+ <template v-if="!isLoading">
<gl-alert
v-if="showDisabledFormMessage"
:dismissible="false"
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
index b003b6aeb6b..1dd88d69d30 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -110,7 +110,7 @@ export default {
{{ cleanupRulesButtonText }}
</gl-button>
</gl-card>
- <template v-else>
+ <template v-if="!$apollo.queries.containerExpirationPolicy.loading">
<gl-alert
v-if="showDisabledFormMessage"
:dismissible="false"
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js b/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
index 8cecc1d3ef7..97fb64f9971 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_downloader.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -29,7 +29,7 @@ export default class PayloadDownloader {
PayloadDownloader.downloadFile(data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Error fetching payload data.'),
});
})
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index 616005565c4..1cd19fc09a8 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -43,7 +43,7 @@ export default class PayloadPreviewer {
})
.catch(() => {
this.spinner.classList.remove('gl-display-inline');
- createFlash({
+ createAlert({
message: __('Error fetching payload data.'),
});
});
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index 18ba89f8856..40348e0b18a 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
@@ -30,7 +30,7 @@ export default () => {
$jsBroadcastMessagePreview.html(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while rendering preview broadcast message'),
}),
);
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
index f687423594d..ffd976be8c6 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
@@ -1,5 +1,10 @@
+import initBroadcastMessages from '~/admin/broadcast_messages';
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import initBroadcastMessagesForm from './broadcast_message';
-initBroadcastMessagesForm();
-initDeprecatedRemoveRowBehavior();
+if (gon.features.vueBroadcastMessages) {
+ initBroadcastMessages();
+} else {
+ initBroadcastMessagesForm();
+ initDeprecatedRemoveRowBehavior();
+}
diff --git a/app/assets/javascripts/pages/admin/dashboard/index.js b/app/assets/javascripts/pages/admin/dashboard/index.js
new file mode 100644
index 00000000000..b63e612be47
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/dashboard/index.js
@@ -0,0 +1,3 @@
+import initGitlabVersionCheck from '~/gitlab_version_check';
+
+initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js
deleted file mode 100644
index 86b80a0ba5b..00000000000
--- a/app/assets/javascripts/pages/admin/groups/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import UsersSelect from '~/users_select';
-
-new UsersSelect(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js
index a249864fa36..eaee625c047 100644
--- a/app/assets/javascripts/pages/admin/index.js
+++ b/app/assets/javascripts/pages/admin/index.js
@@ -1,10 +1,8 @@
-import initGitlabVersionCheck from '~/gitlab_version_check';
import initAdminStatisticsPanel from '~/admin/statistics_panel/index';
import initVueAlerts from '~/vue_alerts';
import initAdmin from './admin';
initVueAlerts();
-initGitlabVersionCheck();
const statisticsPanelContainer = document.getElementById('js-admin-statistics-container');
initAdmin();
diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
index 63b98f4143b..4f42ef2892d 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
+++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlModal } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -31,7 +31,7 @@ export default {
redirectTo(response.request.responseURL);
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('AdminArea|Stopping jobs failed'),
});
throw error;
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index b6f42a27002..2a7619da8cc 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import { getGroups } from '~/api/groups_api';
import { getProjects } from '~/api/projects_api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -119,7 +119,7 @@ export default class Todos {
})
.catch(() => {
this.updateRowState(target, true);
- return createFlash({
+ return createAlert({
message: __('Error updating status of to-do item.'),
});
});
@@ -168,7 +168,7 @@ export default class Todos {
this.updateBadges(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error updating status for all to-do items.'),
}),
);
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index de28f027126..377ba0f13a9 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,6 +1,10 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
+import {
+ initBulkUpdateSidebar,
+ initStatusDropdown,
+ initSubscriptionsDropdown,
+} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
@@ -9,6 +13,8 @@ const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
+initStatusDropdown();
+initSubscriptionsDropdown();
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 8ce73be6e74..fa111032b2e 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
import { getGroupPathAvailability } from '~/rest_api';
@@ -62,7 +62,7 @@ export default class GroupPathValidator extends InputValidator {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while validating group path'),
}),
);
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 7dab5258b24..a555038ed5c 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import BindInOut from '~/behaviors/bind_in_out';
import initFilePickers from '~/file_pickers';
import Group from '~/group';
@@ -8,6 +10,8 @@ import NewGroupCreationApp from './components/app.vue';
import GroupPathValidator from './group_path_validator';
import initToggleInviteMembers from './toggle_invite_members';
+Vue.use(VueApollo);
+
new GroupPathValidator(); // eslint-disable-line no-new
new Group(); // eslint-disable-line no-new
initGroupNameAndPath();
@@ -31,8 +35,13 @@ function initNewGroupCreation(el) {
hasErrors: parseBoolean(hasErrors),
};
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
return new Vue({
el,
+ apolloProvider,
provide: {
verificationRequired: parseBoolean(verificationRequired),
verificationFormUrl,
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 bf77d968e7d..b1a1cc21764 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
@@ -2,9 +2,11 @@ import initStaleRunnerCleanupSetting from 'ee_else_ce/group_settings/stale_runne
import initVariableList from '~/ci_variable_list';
import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import initSettingsPanels from '~/settings_panels';
+import initDeployTokens from '~/deploy_tokens';
// Initialize expandable settings panels
initSettingsPanels();
+initDeployTokens();
initSharedRunnersForm();
initStaleRunnerCleanupSetting();
diff --git a/app/assets/javascripts/pages/groups/settings/index.js b/app/assets/javascripts/pages/groups/settings/index.js
index cb787c60002..7e97cd865b7 100644
--- a/app/assets/javascripts/pages/groups/settings/index.js
+++ b/app/assets/javascripts/pages/groups/settings/index.js
@@ -1,5 +1,7 @@
import initRevokeButton from '~/deploy_tokens/init_revoke_button';
import initSearchSettings from '~/search_settings';
+import initDeployTokens from '~/deploy_tokens';
+initDeployTokens();
initSearchSettings();
initRevokeButton();
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 9cce6723bf7..6feb4c2188f 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
@@ -2,7 +2,7 @@
import { GlButton, GlEmptyState, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { getBulkImportsHistory } from '~/rest_api';
@@ -107,7 +107,7 @@ export default {
this.pageInfo = parseIntPagination(normalizeHeaders(headers));
this.historyItems = historyItems;
} catch (e) {
- createFlash({ message: DEFAULT_ERROR, captureError: true, error: e });
+ createAlert({ message: DEFAULT_ERROR, captureError: true, error: e });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
new file mode 100644
index 00000000000..20ce296bbec
--- /dev/null
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlAvatarLabeled, GlListbox } from '@gitlab/ui';
+import { __ } from '~/locale';
+import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const USERS_PER_PAGE = 20;
+
+export default {
+ components: {
+ GlAvatarLabeled,
+ GlListbox,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ usersQuery: {
+ query: searchUsersQuery,
+ variables() {
+ return {
+ search: this.search,
+ first: USERS_PER_PAGE,
+ };
+ },
+ update(data) {
+ return data;
+ },
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ },
+ },
+ data() {
+ return {
+ user: '',
+ search: '',
+ };
+ },
+ computed: {
+ userId() {
+ return getIdFromGraphQLId(this.user);
+ },
+ users() {
+ return [
+ { text: __('(no user)'), value: '' },
+ ...(this.usersQuery?.users.nodes || []).map((u) => ({
+ username: `@${u.username}`,
+ avatarUrl: u.avatarUrl,
+ text: u.name,
+ value: u.id,
+ })),
+ ];
+ },
+ },
+ methods: {
+ clearTransform() {
+ // FIXME: workaround for listbox issue
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1986
+ const { listbox } = this.$refs;
+ if (listbox.querySelector('.dropdown-menu')) {
+ listbox.querySelector('.dropdown-menu').style.transform = '';
+ }
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-listbox
+ ref="listbox"
+ v-model="user"
+ :items="users"
+ searchable
+ is-check-centered
+ :searching="$apollo.loading"
+ @click.capture.native="clearTransform"
+ @search="search = $event"
+ >
+ <template #list-item="{ item }">
+ <gl-avatar-labeled
+ shape="circle"
+ :size="32"
+ :src="item.avatarUrl"
+ :label="item.text"
+ :sub-label="item.username"
+ />
+ </template>
+ </gl-listbox>
+ <input type="hidden" :name="name" :value="userId" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
index 86b80a0ba5b..ef549f20cf3 100644
--- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/index.js
@@ -1,3 +1,19 @@
-import UsersSelect from '~/users_select';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import UserSelect from './components/user_select.vue';
-new UsersSelect(); // eslint-disable-line no-new
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+Array.from(document.querySelectorAll('.js-gitlab-user')).forEach(
+ (node) =>
+ new Vue({
+ el: node,
+ apolloProvider,
+ render: (h) => h(UserSelect, { props: { name: node.dataset.name } }),
+ }),
+);
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index db6f0c23dbd..09b1b3a9c0f 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { s__, __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getProjects } from '~/rest_api';
import ImportStatus from '~/import_entities/components/import_status.vue';
@@ -104,7 +104,7 @@ export default {
this.pageInfo = parseIntPagination(normalizeHeaders(headers));
this.historyItems = historyItems;
} catch (e) {
- createFlash({ message: DEFAULT_ERROR, captureError: true, error: e });
+ createAlert({ message: DEFAULT_ERROR, captureError: true, error: e });
} finally {
this.loading = false;
}
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index 6afb3636998..91b20a05196 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -3,6 +3,7 @@ import '~/profile/gl_crop';
import Profile from '~/profile/profile';
import initSearchSettings from '~/search_settings';
import initPasswordPrompt from './password_prompt';
+import { initTimezoneDropdown } from './init_timezone_dropdown';
// eslint-disable-next-line func-names
$(document).on('input.ssh_key', '#key_key', function () {
@@ -21,3 +22,4 @@ new Profile(); // eslint-disable-line no-new
initSearchSettings();
initPasswordPrompt();
+initTimezoneDropdown();
diff --git a/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js b/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
new file mode 100644
index 00000000000..80b911493a8
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+
+export const initTimezoneDropdown = () => {
+ const el = document.querySelector('.js-timezone-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ const { timezoneData, initialValue } = el.dataset;
+ const timezones = JSON.parse(timezoneData);
+
+ const timezoneDropdown = new Vue({
+ el,
+ data() {
+ return {
+ value: initialValue,
+ };
+ },
+ render(h) {
+ return h(TimezoneDropdown, {
+ props: {
+ value: this.value,
+ timezoneData: timezones,
+ name: 'user[timezone]',
+ },
+ class: 'gl-md-form-input-lg',
+ });
+ },
+ });
+
+ return timezoneDropdown;
+};
diff --git a/app/assets/javascripts/pages/projects/blame/show/index.js b/app/assets/javascripts/pages/projects/blame/show/index.js
index fa22c11d1d7..1e4b9de90f2 100644
--- a/app/assets/javascripts/pages/projects/blame/show/index.js
+++ b/app/assets/javascripts/pages/projects/blame/show/index.js
@@ -1,3 +1,5 @@
import initBlob from '~/pages/projects/init_blob';
+import redirectToCorrectPage from '~/blame/blame_redirect';
+redirectToCorrectPage();
initBlob();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index eca3cf7ab13..af0097b415c 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -4,7 +4,7 @@ import Vue from 'vue';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import initDeprecatedNotes from '~/init_deprecated_notes';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import axios from '~/lib/utils/axios_utils';
@@ -69,7 +69,7 @@ if (filesContainer.length) {
loadDiffStats();
})
.catch(() => {
- createFlash({ message: __('An error occurred while retrieving diff files') });
+ createAlert({ message: __('An error occurred while retrieving diff files') });
});
} else {
new Diff();
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 b415e36bf09..30cefa3d717 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
@@ -12,7 +12,7 @@ import {
} from '@gitlab/ui';
import { kebabCase } from 'lodash';
import { buildApiUrl } from '~/api/api_utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -220,7 +220,7 @@ export default {
redirectTo(data.web_url);
return;
} catch (error) {
- createFlash({
+ createAlert({
message: s__(
'ForkProject|An error occurred while forking the project. Please try again.',
),
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 2b3055ecd66..00e0649deed 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
@@ -9,7 +9,7 @@ import {
GlSearchBoxByType,
GlTruncate,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -41,7 +41,7 @@ export default {
return length > 0 && length < MINIMUM_SEARCH_LENGTH;
},
error(error) {
- createFlash({
+ createAlert({
message: s__(
'ForkProject|Something went wrong while loading data. Please refresh the page to try again.',
),
diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js
index 2a120a690ef..ed476d25f8b 100644
--- a/app/assets/javascripts/pages/projects/hooks/index.js
+++ b/app/assets/javascripts/pages/projects/hooks/index.js
@@ -1,3 +1,5 @@
import initSearchSettings from '~/search_settings';
+import initWebhookForm from '~/webhooks';
initSearchSettings();
+initWebhookForm();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
index 9a38c2cc765..65942464e2b 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
@@ -39,7 +39,7 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching refs'),
}),
);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
index 5179d1b31ab..406959c80ea 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/edit/index.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -33,7 +33,7 @@ function initTargetBranchSelector() {
callback(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error fetching branches'),
}),
);
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index e284e7b2c5e..2399aafc9b5 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -2,14 +2,19 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
+import {
+ initBulkUpdateSidebar,
+ initStatusDropdown,
+ initSubscriptionsDropdown,
+} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
-initIssueStatusSelect();
+initStatusDropdown();
+initSubscriptionsDropdown();
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
index 6dd21380bec..0edce2db0a3 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -1,3 +1,8 @@
+import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
-initForm();
+if (gon.features?.pipelineSchedulesVue) {
+ initPipelineSchedulesFormApp('#pipeline-schedules-form-edit');
+} else {
+ initForm();
+}
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 9513f42d9c9..7d0930f6424 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js
@@ -1,9 +1,10 @@
import Vue from 'vue';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
+import initPipelineSchedulesApp from '~/pipeline_schedules/mount_pipeline_schedules_app';
import PipelineSchedulesTakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue';
import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue';
-function initPipelineSchedules() {
+function initPipelineSchedulesCallout() {
const el = document.getElementById('pipeline-schedules-callout');
if (!el) {
@@ -15,6 +16,7 @@ function initPipelineSchedules() {
// eslint-disable-next-line no-new
new Vue({
el,
+ name: 'PipelineSchedulesCalloutRoot',
provide: {
docsUrl,
illustrationUrl,
@@ -25,6 +27,8 @@ function initPipelineSchedules() {
});
}
+// TODO: move take ownership feature into new Vue app
+// located in directory app/assets/javascripts/pipeline_schedules/components
function initTakeownershipModal() {
const modalId = 'pipeline-take-ownership-modal';
const buttonSelector = 'js-take-ownership-button';
@@ -63,5 +67,10 @@ function initTakeownershipModal() {
});
}
-initPipelineSchedules();
-initTakeownershipModal();
+initPipelineSchedulesCallout();
+
+if (gon.features?.pipelineSchedulesVue) {
+ initPipelineSchedulesApp();
+} else {
+ initTakeownershipModal();
+}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
index 6dd21380bec..06084fa729b 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -1,3 +1,8 @@
+import initPipelineSchedulesFormApp from '~/pipeline_schedules/mount_pipeline_schedules_form_app';
import initForm from '../shared/init_form';
-initForm();
+if (gon.features?.pipelineSchedulesVue) {
+ initPipelineSchedulesFormApp('#pipeline-schedules-form-new');
+} else {
+ initForm();
+}
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index 277d2e0d30a..bc467952551 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,4 +1,5 @@
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
const defaultTimezone = { identifier: 'Etc/UTC', name: 'UTC', offset: 0 };
const defaults = {
@@ -17,8 +18,6 @@ export const formatUtcOffset = (offset) => {
return `${prefix} ${Math.abs(offset / 3600)}`;
};
-export const formatTimezone = (item) => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`;
-
export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
if (tzList && tzList.length && identifier && identifier.length) {
return tzList.find((tz) => tz.identifier === identifier) || null;
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index ccabaad5b2e..d177c67f133 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import { setCookie } from '~/lib/utils/common_utils';
import initClonePanel from '~/clone_panel';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { serializeForm } from '~/lib/utils/forms';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -80,7 +80,7 @@ export default class Project {
})
.then(({ data }) => callback(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while getting projects'),
}),
);
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 6a9bd34db22..8909ff1f221 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
@@ -10,6 +10,7 @@ import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_to
import initSettingsPanels from '~/settings_panels';
import { initTokenAccess } from '~/token_access';
import { initCiSecureFiles } from '~/ci_secure_files';
+import initDeployTokens from '~/deploy_tokens';
// Initialize expandable settings panels
initSettingsPanels();
@@ -34,6 +35,7 @@ document.querySelector('.js-toggle-extra-settings').addEventListener('click', (e
});
registrySettingsApp();
+initDeployTokens();
initDeployFreeze();
initSettingsPipelinesTriggers();
diff --git a/app/assets/javascripts/pages/projects/settings/index.js b/app/assets/javascripts/pages/projects/settings/index.js
index cb787c60002..7e97cd865b7 100644
--- a/app/assets/javascripts/pages/projects/settings/index.js
+++ b/app/assets/javascripts/pages/projects/settings/index.js
@@ -1,5 +1,7 @@
import initRevokeButton from '~/deploy_tokens/init_revoke_button';
import initSearchSettings from '~/search_settings';
+import initDeployTokens from '~/deploy_tokens';
+initDeployTokens();
initSearchSettings();
initRevokeButton();
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index 655243eee30..d2263fa815d 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,5 +1,7 @@
import MirrorRepos from '~/mirrors/mirror_repos';
import mountBranchRules from '~/projects/settings/repository/branch_rules/mount_branch_rules';
+import mountDefaultBranchSelector from '~/projects/settings/mount_default_branch_selector';
+
import initForm from '../form';
initForm();
@@ -8,3 +10,4 @@ const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
mountBranchRules(document.getElementById('js-branch-rules'));
+mountDefaultBranchSelector(document.querySelector('.js-select-default-branch'));
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index a82f485bf44..3e5c02bbf19 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -898,7 +898,9 @@ export default {
:help-path="pagesHelpPath"
:label="$options.i18n.pagesLabel"
:help-text="
- s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
+ s__(
+ 'ProjectSettings|With GitLab Pages you can host your static websites on GitLab. GitLab Pages uses a caching mechanism for efficiency. Your changes may not take effect until that cache is invalidated, which usually takes less than a minute.',
+ )
"
>
<project-feature-setting
@@ -979,20 +981,20 @@ export default {
name="project[project_feature_attributes][feature_flags_access_level]"
/>
</project-setting-row>
- <project-setting-row
- ref="releases-settings"
- :label="$options.i18n.releasesLabel"
- :help-text="$options.i18n.releasesHelpText"
- :help-path="releasesHelpPath"
- >
- <project-feature-setting
- v-model="releasesAccessLevel"
- :label="$options.i18n.releasesLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][releases_access_level]"
- />
- </project-setting-row>
</template>
+ <project-setting-row
+ ref="releases-settings"
+ :label="$options.i18n.releasesLabel"
+ :help-text="$options.i18n.releasesHelpText"
+ :help-path="releasesHelpPath"
+ >
+ <project-feature-setting
+ v-model="releasesAccessLevel"
+ :label="$options.i18n.releasesLabel"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][releases_access_level]"
+ />
+ </project-setting-row>
</div>
<project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3">
<label class="js-emails-disabled">
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index 7ea744a68a6..1848aa70cf0 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
@@ -51,7 +51,7 @@ export default class UsernameValidator extends InputValidator {
);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while validating username'),
}),
);
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
index 10b95fd6f3c..b72579276e8 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
@@ -1,6 +1,6 @@
<script>
import { GlSkeletonLoader, GlSafeHtmlDirective, GlAlert } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
@@ -47,7 +47,7 @@ export default {
handleLocationHash();
})
.catch(() =>
- createFlash({
+ createAlert({
message: this.$options.i18n.renderingContentFailed,
}),
);
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 9acc1cb62a1..7b9656de362 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -8,21 +8,19 @@ import {
GlFormGroup,
GlFormInput,
GlFormSelect,
- GlSegmentedControl,
} from '@gitlab/ui';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import axios from '~/lib/utils/axios_utils';
+import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
import csrf from '~/lib/utils/csrf';
import { setUrlFragment } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import {
- CONTENT_EDITOR_LOADED_ACTION,
SAVED_USING_CONTENT_EDITOR_ACTION,
WIKI_CONTENT_EDITOR_TRACKING_LABEL,
WIKI_FORMAT_LABEL,
WIKI_FORMAT_UPDATED_ACTION,
+ CONTENT_EDITOR_LOADED_ACTION,
} from '../constants';
const trackingMixin = Tracking.mixin({
@@ -36,6 +34,29 @@ const MARKDOWN_LINK_TEXT = {
org: '[[page-slug]]',
};
+function getPagePath(pageInfo) {
+ return pageInfo.persisted ? pageInfo.path : pageInfo.createPath;
+}
+
+const autosaveKey = (pageInfo, field) => {
+ const path = pageInfo.persisted ? pageInfo.path : pageInfo.createPath;
+
+ return `${path}/${field}`;
+};
+
+const titleAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'title');
+const formatAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'format');
+const contentAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'content');
+const commitAutosaveKey = (pageInfo) => autosaveKey(pageInfo, 'commit');
+
+const getTitle = (pageInfo) => getDraft(titleAutosaveKey(pageInfo)) || pageInfo.title?.trim() || '';
+const getFormat = (pageInfo) =>
+ getDraft(formatAutosaveKey(pageInfo)) || pageInfo.format || 'markdown';
+const getContent = (pageInfo) => getDraft(contentAutosaveKey(pageInfo)) || pageInfo.content || '';
+const getCommitMessage = (pageInfo) =>
+ getDraft(commitAutosaveKey(pageInfo)) || pageInfo.commitMessage || '';
+const getIsFormDirty = (pageInfo) => Boolean(getDraft(titleAutosaveKey(pageInfo)));
+
export default {
i18n: {
title: {
@@ -74,10 +95,6 @@ export default {
},
cancel: s__('WikiPage|Cancel'),
},
- switchEditingControlOptions: [
- { text: s__('Wiki Page|Source'), value: 'source' },
- { text: s__('Wiki Page|Rich text'), value: 'richText' },
- ],
components: {
GlIcon,
GlForm,
@@ -87,26 +104,21 @@ export default {
GlSprintf,
GlLink,
GlButton,
- GlSegmentedControl,
- MarkdownField,
- LocalStorageSync,
- ContentEditor: () =>
- import(
- /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
- ),
+ MarkdownEditor,
},
mixins: [trackingMixin],
inject: ['formatOptions', 'pageInfo'],
data() {
return {
editingMode: 'source',
- title: this.pageInfo.title?.trim() || '',
- format: this.pageInfo.format || 'markdown',
- content: this.pageInfo.content || '',
- commitMessage: '',
- isDirty: false,
+ title: getTitle(this.pageInfo),
+ format: getFormat(this.pageInfo),
+ content: getContent(this.pageInfo),
+ commitMessage: getCommitMessage(this.pageInfo),
contentEditorEmpty: false,
+ isContentEditorActive: false,
switchEditingControlDisabled: false,
+ isFormDirty: getIsFormDirty(this.pageInfo),
};
},
computed: {
@@ -117,7 +129,7 @@ export default {
return csrf.token;
},
formAction() {
- return this.pageInfo.persisted ? this.pageInfo.path : this.pageInfo.createPath;
+ return getPagePath(this.pageInfo);
},
helpPath() {
return setUrlFragment(
@@ -162,15 +174,9 @@ export default {
disableSubmitButton() {
return this.noContent || !this.title;
},
- isContentEditorActive() {
- return this.isMarkdownFormat && this.useContentEditor;
- },
- useContentEditor() {
- return this.editingMode === 'richText';
- },
},
mounted() {
- this.updateCommitMessage();
+ if (!this.commitMessage) this.updateCommitMessage();
window.addEventListener('beforeunload', this.onPageUnload);
},
@@ -178,51 +184,45 @@ export default {
window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
- renderMarkdown(content) {
- return axios
- .post(this.pageInfo.markdownPreviewPath, { text: content })
- .then(({ data }) => data.body);
- },
-
- setEditingMode(editingMode) {
- this.editingMode = editingMode;
- },
-
async handleFormSubmit(e) {
- e.preventDefault();
+ this.isFormDirty = false;
- if (this.useContentEditor) {
- this.trackFormSubmit();
- }
+ e.preventDefault();
+ this.trackFormSubmit();
this.trackWikiFormat();
// Wait until form field values are refreshed
await this.$nextTick();
e.target.submit();
+ },
- this.isDirty = false;
+ updateDrafts() {
+ updateDraft(titleAutosaveKey(this.pageInfo), this.title);
+ updateDraft(formatAutosaveKey(this.pageInfo), this.format);
+ updateDraft(contentAutosaveKey(this.pageInfo), this.content);
+ updateDraft(commitAutosaveKey(this.pageInfo), this.commitMessage);
},
- handleContentChange() {
- this.isDirty = true;
+ clearDrafts() {
+ clearDraft(titleAutosaveKey(this.pageInfo));
+ clearDraft(formatAutosaveKey(this.pageInfo));
+ clearDraft(contentAutosaveKey(this.pageInfo));
+ clearDraft(commitAutosaveKey(this.pageInfo));
},
- handleContentEditorChange({ empty, markdown, changed }) {
+ handleContentEditorChange({ empty, markdown }) {
this.contentEditorEmpty = empty;
- this.isDirty = changed;
this.content = markdown;
},
- onPageUnload(event) {
- if (!this.isDirty) return undefined;
-
- event.preventDefault();
-
- // eslint-disable-next-line no-param-reassign
- event.returnValue = '';
- return '';
+ onPageUnload() {
+ if (this.isFormDirty) {
+ this.updateDrafts();
+ } else {
+ this.clearDrafts();
+ }
},
updateCommitMessage() {
@@ -235,8 +235,13 @@ export default {
this.commitMessage = newCommitMessage;
},
- trackContentEditorLoaded() {
- this.track(CONTENT_EDITOR_LOADED_ACTION);
+ notifyContentEditorActive() {
+ this.isContentEditorActive = true;
+ this.trackContentEditorLoaded();
+ },
+
+ notifyContentEditorInactive() {
+ this.isContentEditorActive = false;
},
trackFormSubmit() {
@@ -256,12 +261,12 @@ export default {
});
},
- enableSwitchEditingControl() {
- this.switchEditingControlDisabled = false;
+ trackContentEditorLoaded() {
+ this.track(CONTENT_EDITOR_LOADED_ACTION);
},
- disableSwitchEditingControl() {
- this.switchEditingControlDisabled = true;
+ submitFormWithShortcut() {
+ this.$refs.form.submit();
},
},
};
@@ -269,10 +274,12 @@ export default {
<template>
<gl-form
+ ref="form"
:action="formAction"
method="post"
class="wiki-form common-note-form gl-mt-3 js-quick-submit"
@submit="handleFormSubmit"
+ @input="isFormDirty = true"
>
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
@@ -329,74 +336,23 @@ export default {
<div class="row" data-testid="wiki-form-content-fieldset">
<div class="col-sm-12 row-sm-5">
<gl-form-group>
- <div v-if="isMarkdownFormat" class="gl-display-flex gl-justify-content-start gl-mb-3">
- <gl-segmented-control
- data-testid="toggle-editing-mode-button"
- data-qa-selector="editing_mode_button"
- class="gl-display-flex"
- :checked="editingMode"
- :options="$options.switchEditingControlOptions"
- :disabled="switchEditingControlDisabled"
- @input="setEditingMode"
- />
- </div>
- <local-storage-sync
- storage-key="gl-wiki-content-editor-enabled"
- :value="editingMode"
- @input="setEditingMode"
- />
- <markdown-field
- v-if="!isContentEditorActive"
- :markdown-preview-path="pageInfo.markdownPreviewPath"
- :can-attach-file="true"
- :enable-autocomplete="true"
- :textarea-value="content"
+ <markdown-editor
+ v-model="content"
+ :render-markdown-path="pageInfo.markdownPreviewPath"
:markdown-docs-path="pageInfo.markdownHelpPath"
:uploads-path="pageInfo.uploadsPath"
+ :enable-content-editor="isMarkdownFormat"
:enable-preview="isMarkdownFormat"
- class="bordered-box"
- >
- <template #textarea>
- <textarea
- id="wiki_content"
- ref="textarea"
- v-model="content"
- name="wiki[content]"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- dir="auto"
- data-supports-quick-actions="false"
- data-qa-selector="wiki_content_textarea"
- :autofocus="pageInfo.persisted"
- :aria-label="$options.i18n.content.label"
- :placeholder="$options.i18n.content.placeholder"
- @input="handleContentChange"
- >
- </textarea>
- </template>
- </markdown-field>
- <div v-if="isContentEditorActive">
- <content-editor
- :render-markdown="renderMarkdown"
- :uploads-path="pageInfo.uploadsPath"
- :markdown="content"
- @initialized="trackContentEditorLoaded"
- @change="handleContentEditorChange"
- @loading="disableSwitchEditingControl"
- @loadingSuccess="enableSwitchEditingControl"
- @loadingError="enableSwitchEditingControl"
- />
- <input
- id="wiki_content"
- v-model.trim="content"
- type="hidden"
- name="wiki[content]"
- data-qa-selector="wiki_hidden_content"
- />
- </div>
-
- <div class="clearfix"></div>
- <div class="error-alert"></div>
-
+ :init-on-autofocus="pageInfo.persisted"
+ :form-field-placeholder="$options.i18n.content.placeholder"
+ :form-field-aria-label="$options.i18n.content.label"
+ form-field-id="wiki_content"
+ form-field-name="wiki[content]"
+ @contentEditor="notifyContentEditorActive"
+ @markdownField="notifyContentEditorInactive"
+ @keydown.ctrl.enter="submitFormShortcut"
+ @keydown.meta.enter="submitFormShortcut"
+ />
<div class="form-text gl-text-gray-600">
<gl-sprintf
v-if="displayWikiSpecificMarkdownHelp"
@@ -447,9 +403,14 @@ export default {
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
- <gl-button data-testid="wiki-cancel-button" :href="cancelFormPath" class="float-right">{{
- $options.i18n.cancel
- }}</gl-button>
+ <gl-button
+ data-testid="wiki-cancel-button"
+ :href="cancelFormPath"
+ class="float-right"
+ @click="isFormDirty = false"
+ >
+ {{ $options.i18n.cancel }}</gl-button
+ >
</div>
</gl-form>
</template>
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 9e0af426f6e..fb761725c43 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -1,7 +1,7 @@
import { select } from 'd3-selection';
import $ from 'jquery';
import { last } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import dateFormat from '~/lib/dateformat';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
@@ -151,7 +151,7 @@ export default class ActivityCalendar {
.select(container)
.append('svg')
.attr('width', width)
- .attr('height', 167)
+ .attr('height', 169)
.attr('class', 'contrib-calendar');
}
@@ -302,7 +302,7 @@ export default class ActivityCalendar {
});
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while retrieving calendar activity'),
}),
);
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
index ddc880db227..f35f9341fa1 100644
--- a/app/assets/javascripts/pdf/index.vue
+++ b/app/assets/javascripts/pdf/index.vue
@@ -1,9 +1,10 @@
<script>
-import pdfjsLib from 'pdfjs-dist/build/pdf';
-import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
+import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist/legacy/build/pdf';
import Page from './page/index.vue';
+GlobalWorkerOptions.workerSrc = '/assets/webpack/pdfjs/pdf.worker.min.js';
+
export default {
components: { Page },
props: {
@@ -30,18 +31,16 @@ export default {
},
watch: { pdf: 'load' },
mounted() {
- pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
if (this.hasPDF) this.load();
},
methods: {
load() {
this.pages = [];
- return pdfjsLib
- .getDocument({
- url: this.document,
- cMapUrl: '/assets/webpack/cmaps/',
- cMapPacked: true,
- })
+ return getDocument({
+ url: this.document,
+ cMapUrl: '/assets/webpack/pdfjs/cmaps/',
+ cMapPacked: true,
+ })
.promise.then(this.renderPages)
.then((pages) => {
this.pages = pages;
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 7e331bdd91d..6ee33902a01 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -1,4 +1,4 @@
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
@@ -73,7 +73,7 @@ export default class PersistentUserCallout {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: __(
'An error occurred while dismissing the alert. Refresh the page and try again.',
),
@@ -94,7 +94,7 @@ export default class PersistentUserCallout {
window.location.assign(href);
})
.catch(() => {
- createFlash({
+ createAlert({
message: __(
'An error occurred while acknowledging the notification. Refresh the page and try again.',
),
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 3e87088e77e..7d2b9cd3d42 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -15,11 +15,20 @@ export default {
'Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started.',
),
btnText: __('Configure pipeline'),
+ externalCiNote: __("This project's pipeline configuration is located outside this repository"),
+ externalCiInstructions: __(
+ 'To edit the pipeline configuration, you must go to the project or external site that hosts the file.',
+ ),
},
inject: {
emptyStateIllustrationPath: {
default: '',
},
+ usesExternalConfig: {
+ default: false,
+ type: Boolean,
+ required: false,
+ },
},
methods: {
createEmptyConfigFile() {
@@ -33,22 +42,31 @@ export default {
<pipeline-editor-file-nav v-on="$listeners" />
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
<img :src="emptyStateIllustrationPath" />
- <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
- <p class="gl-mt-3">
- <gl-sprintf :message="$options.i18n.body">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- <gl-button
- variant="confirm"
- class="gl-mt-3"
- data-qa-selector="create_new_ci_button"
- @click="createEmptyConfigFile"
+ <div
+ v-if="usesExternalConfig"
+ class="gl-display-flex gl-flex-direction-column gl-align-items-center"
>
- {{ $options.i18n.btnText }}
- </gl-button>
+ <h1 class="gl-font-size-h1">{{ $options.i18n.externalCiNote }}</h1>
+ <p class="gl-mt-3">{{ $options.i18n.externalCiInstructions }}</p>
+ </div>
+ <div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center">
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <p class="gl-mt-3">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-button
+ variant="confirm"
+ class="gl-mt-3"
+ data-qa-selector="create_new_ci_button"
+ @click="createEmptyConfigFile"
+ >
+ {{ $options.i18n.btnText }}
+ </gl-button>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index 13dad0b2459..6d91c339833 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { EDITOR_APP_STATUS_LOADING } from './constants';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCurrentBranch from './graphql/queries/client/current_branch.query.graphql';
@@ -42,6 +43,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectNamespace,
simulatePipelineHelpPagePath,
totalBranches,
+ usesExternalConfig,
validateTabIllustrationPath,
ymlHelpPagePath,
} = el.dataset;
@@ -133,6 +135,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
projectNamespace,
simulatePipelineHelpPagePath,
totalBranches: parseInt(totalBranches, 10),
+ usesExternalConfig: parseBoolean(usesExternalConfig),
validateTabIllustrationPath,
ymlHelpPagePath,
},
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 548769eb214..ff848a973e3 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -40,7 +40,7 @@ export default {
PipelineEditorHome,
PipelineEditorMessages,
},
- inject: ['ciConfigPath', 'newMergeRequestPath', 'projectFullPath'],
+ inject: ['ciConfigPath', 'newMergeRequestPath', 'projectFullPath', 'usesExternalConfig'],
data() {
return {
ciConfigData: {},
@@ -397,7 +397,7 @@ export default {
<div class="gl-mt-4 gl-relative" data-qa-selector="pipeline_editor_app">
<gl-loading-icon v-if="isBlobContentLoading" size="lg" class="gl-m-3" />
<pipeline-editor-empty-state
- v-else-if="showStartScreen"
+ v-else-if="showStartScreen || usesExternalConfig"
@createEmptyConfigFile="setNewEmptyCiConfigFile"
@refetchContent="refetchContent"
/>
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 2d5c01a58b7..1972125ed56 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -162,7 +162,7 @@ export default {
</div>
</div>
<commit-section
- v-if="showCommitForm"
+ v-show="showCommitForm"
:ref="$options.commitSectionRef"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
diff --git a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
index 529ec4897b4..cd7cb7f8393 100644
--- a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
@@ -401,7 +401,7 @@ export default {
<div
v-for="(variable, index) in variables"
:key="variable.uniqueId"
- class="gl-mb-3 gl-ml-n3 gl-pb-2"
+ class="gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
data-qa-selector="ci_variable_row_container"
>
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 529ec4897b4..a9af1181027 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -17,17 +17,11 @@ import {
import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
-import { backOff } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
-import {
- VARIABLE_TYPE,
- FILE_TYPE,
- CONFIG_VARIABLES_TIMEOUT,
- CC_VALIDATION_REQUIRED_ERROR,
-} from '../constants';
+import { VARIABLE_TYPE, FILE_TYPE, CC_VALIDATION_REQUIRED_ERROR } from '../constants';
+import createPipelineMutation from '../graphql/mutations/create_pipeline.mutation.graphql';
+import ciConfigVariablesQuery from '../graphql/queries/ci_config_variables.graphql';
import filterVariables from '../utils/filter_variables';
import RefsDropdown from './refs_dropdown.vue';
@@ -76,10 +70,6 @@ export default {
type: String,
required: true,
},
- configVariablesPath: {
- type: String,
- required: true,
- },
defaultBranch: {
type: String,
required: true,
@@ -97,6 +87,10 @@ export default {
required: false,
default: () => ({}),
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
refParam: {
type: String,
required: false,
@@ -116,19 +110,77 @@ export default {
return {
refValue: {
shortName: this.refParam,
+ // this is needed until we add support for ref type in url query strings
+ // ensure default branch is called with full ref on load
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
+ fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
},
form: {},
errorTitle: null,
error: null,
+ predefinedValueOptions: {},
warnings: [],
totalWarnings: 0,
isWarningDismissed: false,
- isLoading: false,
submitted: false,
ccAlertDismissed: false,
};
},
+ apollo: {
+ ciConfigVariables: {
+ query: ciConfigVariablesQuery,
+ // Skip when variables already cached in `form`
+ skip() {
+ return Object.keys(this.form).includes(this.refFullName);
+ },
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ ref: this.refQueryParam,
+ };
+ },
+ update({ project }) {
+ return project?.ciConfigVariables || [];
+ },
+ result({ data }) {
+ const predefinedVars = data?.project?.ciConfigVariables || [];
+ const params = {};
+ const descriptions = {};
+
+ predefinedVars.forEach(({ description, key, value, valueOptions }) => {
+ if (description) {
+ params[key] = value;
+ descriptions[key] = description;
+ this.predefinedValueOptions[key] = valueOptions;
+ }
+ });
+
+ Vue.set(this.form, this.refFullName, { descriptions, variables: [] });
+
+ // Add default variables from yml
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
+
+ // Add/update variables, e.g. from query string
+ if (this.variableParams) {
+ this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
+ }
+
+ if (this.fileParams) {
+ this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
+ }
+
+ // Adds empty var at the end of the form
+ this.addEmptyVariable(this.refFullName);
+ },
+ error(error) {
+ Sentry.captureException(error);
+ },
+ },
+ },
computed: {
+ isLoading() {
+ return this.$apollo.queries.ciConfigVariables.loading;
+ },
overMaxWarningsLimit() {
return this.totalWarnings > this.maxWarnings;
},
@@ -147,6 +199,9 @@ export default {
refFullName() {
return this.refValue.fullName;
},
+ refQueryParam() {
+ return this.refFullName || this.refShortName;
+ },
variables() {
return this.form[this.refFullName]?.variables ?? [];
},
@@ -157,21 +212,6 @@ export default {
return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
},
},
- watch: {
- refValue() {
- this.loadConfigVariablesForm();
- },
- },
- created() {
- // this is needed until we add support for ref type in url query strings
- // ensure default branch is called with full ref on load
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- if (this.refValue.shortName === this.defaultBranch) {
- this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
- }
-
- this.loadConfigVariablesForm();
- },
methods: {
addEmptyVariable(refValue) {
const { variables } = this.form[refValue];
@@ -204,132 +244,57 @@ export default {
});
}
},
- setVariableType(key, type) {
+ setVariableAttribute(key, attribute, value) {
const { variables } = this.form[this.refFullName];
const variable = variables.find((v) => v.key === key);
- variable.variable_type = type;
+ variable[attribute] = value;
},
setVariableParams(refValue, type, paramsObj) {
Object.entries(paramsObj).forEach(([key, value]) => {
this.setVariable(refValue, type, key, value);
});
},
+ shouldShowValuesDropdown(key) {
+ return this.predefinedValueOptions[key]?.length > 1;
+ },
removeVariable(index) {
this.variables.splice(index, 1);
},
canRemove(index) {
return index < this.variables.length - 1;
},
- loadConfigVariablesForm() {
- // Skip when variables already cached in `form`
- if (this.form[this.refFullName]) {
- return;
- }
-
- this.fetchConfigVariables(this.refFullName || this.refShortName)
- .then(({ descriptions, params }) => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions,
- });
-
- // Add default variables from yml
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
- })
- .catch(() => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions: {},
- });
- })
- .finally(() => {
- // Add/update variables, e.g. from query string
- if (this.variableParams) {
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
- }
- if (this.fileParams) {
- this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
- }
-
- // Adds empty var at the end of the form
- this.addEmptyVariable(this.refFullName);
- });
- },
- fetchConfigVariables(refValue) {
- this.isLoading = true;
-
- return backOff((next, stop) => {
- axios
- .get(this.configVariablesPath, {
- params: {
- sha: refValue,
- },
- })
- .then(({ data, status }) => {
- if (status === httpStatusCodes.NO_CONTENT) {
- next();
- } else {
- this.isLoading = false;
- stop(data);
- }
- })
- .catch((error) => {
- stop(error);
- });
- }, CONFIG_VARIABLES_TIMEOUT)
- .then((data) => {
- const params = {};
- const descriptions = {};
-
- Object.entries(data).forEach(([key, { value, description }]) => {
- if (description) {
- params[key] = value;
- descriptions[key] = description;
- }
- });
-
- return { params, descriptions };
- })
- .catch((error) => {
- this.isLoading = false;
-
- Sentry.captureException(error);
-
- return { params: {}, descriptions: {} };
- });
- },
- createPipeline() {
+ async createPipeline() {
this.submitted = true;
this.ccAlertDismissed = false;
- return axios
- .post(this.pipelinesPath, {
+ const { data } = await this.$apollo.mutate({
+ mutation: createPipelineMutation,
+ variables: {
+ endpoint: this.pipelinesPath,
// send shortName as fall back for query params
// https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- ref: this.refValue.fullName || this.refShortName,
- variables_attributes: filterVariables(this.variables),
- })
- .then(({ data }) => {
- redirectTo(`${this.pipelinesPath}/${data.id}`);
- })
- .catch((err) => {
- // always re-enable submit button
- this.submitted = false;
+ ref: this.refQueryParam,
+ variablesAttributes: filterVariables(this.variables),
+ },
+ });
- const {
- errors = [],
- warnings = [],
- total_warnings: totalWarnings = 0,
- } = err.response.data;
- const [error] = errors;
+ const { id, errors, totalWarnings, warnings } = data.createPipeline;
- this.reportError({
- title: i18n.submitErrorTitle,
- error,
- warnings,
- totalWarnings,
- });
- });
+ if (id) {
+ redirectTo(`${this.pipelinesPath}/${id}`);
+ return;
+ }
+
+ // always re-enable submit button
+ this.submitted = false;
+ const [error] = errors;
+
+ this.reportError({
+ title: i18n.submitErrorTitle,
+ error,
+ warnings,
+ totalWarnings,
+ });
},
onRefsLoadingError(error) {
this.reportError({ title: i18n.refsLoadingErrorTitle });
@@ -401,7 +366,7 @@ export default {
<div
v-for="(variable, index) in variables"
:key="variable.uniqueId"
- class="gl-mb-3 gl-ml-n3 gl-pb-2"
+ class="gl-mb-3 gl-pb-2"
data-testid="ci-variable-row"
data-qa-selector="ci_variable_row_container"
>
@@ -416,7 +381,7 @@ export default {
<gl-dropdown-item
v-for="type in Object.keys($options.typeOptions)"
:key="type"
- @click="setVariableType(variable.key, type)"
+ @click="setVariableAttribute(variable.key, 'variable_type', type)"
>
{{ $options.typeOptions[type] }}
</gl-dropdown-item>
@@ -429,7 +394,24 @@ export default {
data-qa-selector="ci_variable_key_field"
@change="addEmptyVariable(refFullName)"
/>
+ <gl-dropdown
+ v-if="shouldShowValuesDropdown(variable.key)"
+ :text="variable.value"
+ :class="$options.formElementClasses"
+ class="gl-flex-grow-1 gl-mr-0!"
+ data-testid="pipeline-form-ci-variable-value-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="value in predefinedValueOptions[variable.key]"
+ :key="value"
+ data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ @click="setVariableAttribute(variable.key, 'value', value)"
+ >
+ {{ value }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<gl-form-textarea
+ v-else
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3"
diff --git a/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql b/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
new file mode 100644
index 00000000000..a76e8f6b95b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/graphql/mutations/create_pipeline.mutation.graphql
@@ -0,0 +1,9 @@
+mutation createPipeline($endpoint: String, $ref: String, $variablesAttributes: Array) {
+ createPipeline(endpoint: $endpoint, ref: $ref, variablesAttributes: $variablesAttributes)
+ @client {
+ id
+ errors
+ totalWarnings
+ warnings
+ }
+}
diff --git a/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql b/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql
new file mode 100644
index 00000000000..648cd8b66b5
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/graphql/queries/ci_config_variables.graphql
@@ -0,0 +1,11 @@
+query ciConfigVariables($fullPath: ID!, $ref: String!) {
+ project(fullPath: $fullPath) {
+ id
+ ciConfigVariables(sha: $ref) {
+ description
+ key
+ value
+ valueOptions
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_new/graphql/resolvers.js b/app/assets/javascripts/pipeline_new/graphql/resolvers.js
new file mode 100644
index 00000000000..7b0f58e8cf9
--- /dev/null
+++ b/app/assets/javascripts/pipeline_new/graphql/resolvers.js
@@ -0,0 +1,29 @@
+import axios from '~/lib/utils/axios_utils';
+
+export const resolvers = {
+ Mutation: {
+ createPipeline: (_, { endpoint, ref, variablesAttributes }) => {
+ return axios
+ .post(endpoint, { ref, variables_attributes: variablesAttributes })
+ .then((response) => {
+ const { id } = response.data;
+ return {
+ id,
+ errors: [],
+ totalWarnings: 0,
+ warnings: [],
+ };
+ })
+ .catch((err) => {
+ const { errors = [], totalWarnings = 0, warnings = [] } = err.response.data;
+
+ return {
+ id: null,
+ errors,
+ totalWarnings,
+ warnings,
+ };
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index e3f363f4ada..60b4c93d1d5 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
+import { resolvers } from './graphql/resolvers';
const mountLegacyPipelineNewForm = (el) => {
const {
@@ -51,12 +54,12 @@ const mountPipelineNewForm = (el) => {
projectRefsEndpoint,
// props
- configVariablesPath,
defaultBranch,
fileParam,
maxWarnings,
pipelinesPath,
projectId,
+ projectPath,
refParam,
settingsLink,
varParam,
@@ -65,22 +68,27 @@ const mountPipelineNewForm = (el) => {
const variableParams = JSON.parse(varParam);
const fileParams = JSON.parse(fileParam);
- // TODO: add apolloProvider
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(resolvers),
+ });
return new Vue({
el,
+ apolloProvider,
provide: {
projectRefsEndpoint,
},
render(createElement) {
return createElement(PipelineNewForm, {
props: {
- configVariablesPath,
defaultBranch,
fileParams,
maxWarnings: Number(maxWarnings),
pipelinesPath,
projectId,
+ projectPath,
refParam,
settingsLink,
variableParams,
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue
new file mode 100644
index 00000000000..4a08a82275a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
+import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
+
+export default {
+ i18n: {
+ schedulesFetchError: s__('PipelineSchedules|There was a problem fetching pipeline schedules.'),
+ scheduleDeleteError: s__(
+ 'PipelineSchedules|There was a problem deleting the pipeline schedule.',
+ ),
+ },
+ modal: {
+ id: 'delete-pipeline-schedule-modal',
+ deleteConfirmation: s__(
+ 'PipelineSchedules|Are you sure you want to delete this pipeline schedule?',
+ ),
+ actionPrimary: {
+ text: s__('PipelineSchedules|Delete pipeline schedule'),
+ attributes: [{ variant: 'danger' }],
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: [],
+ },
+ },
+ components: {
+ GlAlert,
+ GlLoadingIcon,
+ GlModal,
+ PipelineSchedulesTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ schedules: {
+ query: getPipelineSchedulesQuery,
+ variables() {
+ return {
+ projectPath: this.fullPath,
+ };
+ },
+ update({ project }) {
+ return project?.pipelineSchedules?.nodes || [];
+ },
+ error() {
+ this.reportError(this.$options.i18n.schedulesFetchError);
+ },
+ },
+ },
+ data() {
+ return {
+ schedules: [],
+ hasError: false,
+ errorMessage: '',
+ scheduleToDeleteId: null,
+ showModal: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.schedules.loading;
+ },
+ },
+ methods: {
+ reportError(error) {
+ this.hasError = true;
+ this.errorMessage = error;
+ },
+ showDeleteModal(id) {
+ this.showModal = true;
+ this.scheduleToDeleteId = id;
+ },
+ hideModal() {
+ this.showModal = false;
+ this.scheduleToDeleteId = null;
+ },
+ async deleteSchedule() {
+ try {
+ const {
+ data: {
+ pipelineScheduleDelete: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: deletePipelineScheduleMutation,
+ variables: { id: this.scheduleToDeleteId },
+ });
+
+ if (errors.length > 0) {
+ throw new Error();
+ } else {
+ this.$apollo.queries.schedules.refetch();
+ }
+ } catch {
+ this.reportError(this.$options.i18n.scheduleDeleteError);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="hasError" class="gl-mb-2" variant="danger" @dismiss="hasError = false">
+ {{ errorMessage }}
+ </gl-alert>
+
+ <gl-loading-icon v-if="isLoading" size="lg" />
+
+ <!-- Tabs will be addressed in #371989 -->
+
+ <template v-else>
+ <pipeline-schedules-table :schedules="schedules" @showDeleteModal="showDeleteModal" />
+
+ <gl-modal
+ :visible="showModal"
+ :title="$options.modal.actionPrimary.text"
+ :modal-id="$options.modal.id"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ size="sm"
+ @primary="deleteSchedule"
+ @hide="hideModal"
+ >
+ {{ $options.modal.deleteConfirmation }}
+ </gl-modal>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue
new file mode 100644
index 00000000000..6e24ac6b8d4
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -0,0 +1,18 @@
+<script>
+import { GlForm } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlForm,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form />
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
new file mode 100644
index 00000000000..76d118bf52d
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlButton, GlButtonGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export const i18n = {
+ playTooltip: s__('PipelineSchedules|Run pipeline schedule'),
+ editTooltip: s__('PipelineSchedules|Edit pipeline schedule'),
+ deleteTooltip: s__('PipelineSchedules|Delete pipeline schedule'),
+ takeOwnershipTooltip: s__('PipelineSchedules|Take ownership of pipeline schedule'),
+};
+
+export default {
+ i18n,
+ components: {
+ GlButton,
+ GlButtonGroup,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ canPlay() {
+ return this.schedule.userPermissions.playPipelineSchedule;
+ },
+ canTakeOwnership() {
+ return this.schedule.userPermissions.takeOwnershipPipelineSchedule;
+ },
+ canUpdate() {
+ return this.schedule.userPermissions.updatePipelineSchedule;
+ },
+ canRemove() {
+ return this.schedule.userPermissions.adminPipelineSchedule;
+ },
+ },
+};
+</script>
+
+<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="canTakeOwnership"
+ v-gl-tooltip
+ :title="$options.i18n.takeOwnershipTooltip"
+ icon="user"
+ />
+ <gl-button v-if="canUpdate" v-gl-tooltip :title="$options.i18n.editTooltip" icon="pencil" />
+ <gl-button
+ v-if="canRemove"
+ v-gl-tooltip
+ :title="$options.i18n.deleteTooltip"
+ icon="remove"
+ variant="danger"
+ data-testid="delete-pipeline-schedule-btn"
+ @click="$emit('showDeleteModal', schedule.id)"
+ />
+ </gl-button-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
new file mode 100644
index 00000000000..216796b357c
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -0,0 +1,32 @@
+<script>
+import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+
+export default {
+ components: {
+ CiBadge,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ hasPipeline() {
+ return this.schedule.lastPipeline;
+ },
+ lastPipelineStatus() {
+ return this.schedule?.lastPipeline?.detailedStatus;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <ci-badge v-if="hasPipeline" :status="lastPipelineStatus" class="gl-vertical-align-middle" />
+ <span v-else data-testid="pipeline-schedule-status-text">
+ {{ s__('PipelineSchedules|None') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
new file mode 100644
index 00000000000..48d59bf6e7c
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_next_run.vue
@@ -0,0 +1,32 @@
+<script>
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ TimeAgoTooltip,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ showTimeAgo() {
+ return this.schedule.active && this.schedule.nextRunAt;
+ },
+ realNextRunTime() {
+ return this.schedule.realNextRun;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <time-ago-tooltip v-if="showTimeAgo" :time="realNextRunTime" />
+ <span v-else data-testid="pipeline-schedule-inactive">
+ {{ s__('PipelineSchedules|Inactive') }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
new file mode 100644
index 00000000000..e7fa94eb7fc
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_owner.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAvatar,
+ GlAvatarLink,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ owner() {
+ return this.schedule.owner;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-avatar-link :href="owner.webPath" :title="owner.name" class="gl-ml-3">
+ <gl-avatar :size="32" :src="owner.avatarUrl" />
+ </gl-avatar-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
new file mode 100644
index 00000000000..08efa794bcc
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/cells/pipeline_schedule_target.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ schedule: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ iconName() {
+ return this.schedule.forTag ? 'tag' : 'fork';
+ },
+ refPath() {
+ return this.schedule.refPath;
+ },
+ refDisplay() {
+ return this.schedule.refForDisplay;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-icon :name="iconName" />
+ <span v-if="refPath">
+ <gl-link :href="refPath" class="gl-text-gray-900">{{ refDisplay }}</gl-link>
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue b/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue
new file mode 100644
index 00000000000..d54008b81b2
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/components/table/pipeline_schedules_table.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlTableLite } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue';
+import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue';
+import PipelineScheduleNextRun from './cells/pipeline_schedule_next_run.vue';
+import PipelineScheduleOwner from './cells/pipeline_schedule_owner.vue';
+import PipelineScheduleTarget from './cells/pipeline_schedule_target.vue';
+
+export default {
+ fields: [
+ {
+ key: 'description',
+ label: s__('PipelineSchedules|Description'),
+ columnClass: 'gl-w-40p',
+ },
+ {
+ key: 'target',
+ label: s__('PipelineSchedules|Target'),
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'pipeline',
+ label: s__('PipelineSchedules|Last Pipeline'),
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'next',
+ label: s__('PipelineSchedules|Next Run'),
+ columnClass: 'gl-w-15p',
+ },
+ {
+ key: 'owner',
+ label: s__('PipelineSchedules|Owner'),
+ columnClass: 'gl-w-10p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ columnClass: 'gl-w-15p',
+ },
+ ],
+ components: {
+ GlTableLite,
+ PipelineScheduleActions,
+ PipelineScheduleLastPipeline,
+ PipelineScheduleNextRun,
+ PipelineScheduleOwner,
+ PipelineScheduleTarget,
+ },
+ props: {
+ schedules: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table-lite :fields="$options.fields" :items="schedules" stacked="md">
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(description)="{ item }">
+ <span data-testid="pipeline-schedule-description">
+ {{ item.description }}
+ </span>
+ </template>
+
+ <template #cell(target)="{ item }">
+ <pipeline-schedule-target :schedule="item" />
+ </template>
+
+ <template #cell(pipeline)="{ item }">
+ <pipeline-schedule-last-pipeline :schedule="item" />
+ </template>
+
+ <template #cell(next)="{ item }">
+ <pipeline-schedule-next-run :schedule="item" />
+ </template>
+
+ <template #cell(owner)="{ item }">
+ <pipeline-schedule-owner :schedule="item" />
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <pipeline-schedule-actions
+ :schedule="item"
+ @showDeleteModal="$emit('showDeleteModal', $event)"
+ />
+ </template>
+ </gl-table-lite>
+</template>
diff --git a/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql b/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
new file mode 100644
index 00000000000..8aab0b3fbde
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql
@@ -0,0 +1,6 @@
+mutation deletePipelineSchedule($id: CiPipelineScheduleID!) {
+ pipelineScheduleDelete(input: { id: $id }) {
+ clientMutationId
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql b/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
new file mode 100644
index 00000000000..7d9d658b1b6
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql
@@ -0,0 +1,40 @@
+query getPipelineSchedulesQuery($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ pipelineSchedules {
+ nodes {
+ id
+ description
+ forTag
+ refPath
+ refForDisplay
+ lastPipeline {
+ id
+ detailedStatus {
+ id
+ group
+ icon
+ label
+ text
+ detailsPath
+ }
+ }
+ active
+ nextRunAt
+ realNextRun
+ owner {
+ id
+ avatarUrl
+ name
+ webPath
+ }
+ userPermissions {
+ playPipelineSchedule
+ takeOwnershipPipelineSchedule
+ updatePipelineSchedule
+ adminPipelineSchedule
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js
new file mode 100644
index 00000000000..8f77e06c19a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_app.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import PipelineSchedules from './components/pipeline_schedules.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const containerEl = document.querySelector('#pipeline-schedules-app');
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ name: 'PipelineSchedulesRoot',
+ apolloProvider,
+ provide: {
+ fullPath,
+ },
+ render(createElement) {
+ return createElement(PipelineSchedules);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js
new file mode 100644
index 00000000000..d83417ab84a
--- /dev/null
+++ b/app/assets/javascripts/pipeline_schedules/mount_pipeline_schedules_form_app.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import PipelineSchedulesForm from './components/pipeline_schedules_form.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default (selector) => {
+ const containerEl = document.querySelector(selector);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ name: 'PipelineSchedulesFormRoot',
+ apolloProvider,
+ provide: {
+ fullPath,
+ },
+ render(createElement) {
+ return createElement(PipelineSchedulesForm);
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue b/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue
index a5ce56daf07..bc62d6d4465 100644
--- a/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/widgets/list.vue
@@ -176,7 +176,7 @@ export default {
category="secondary"
data-testid="remove-step-button"
icon="remove"
- @click="removeValue"
+ @click="() => removeValue(i)"
/>
</template>
</gl-form-input-group>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
index 9e886fd7a48..605d40eddee 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
import { prepareFailedJobs } from './utils';
@@ -47,7 +47,7 @@ export default {
this.preparedFailedJobs = prepareFailedJobs(this.failedJobs, this.failedJobsSummary);
},
error() {
- createFlash({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
+ createAlert({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
},
},
},
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 0c6b8b9ed2b..18607bfae1c 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlLink, GlSafeHtmlDirective, GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
@@ -49,7 +49,7 @@ export default {
return job.retryable && job.userPermissions.updateBuild;
},
showErrorMessage() {
- createFlash({ message: s__('Job|There was a problem retrying the failed job.') });
+ createAlert({ message: s__('Job|There was a problem retrying the failed job.') });
},
},
};
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
index 18e9ffa23cf..f1ad312dcaa 100644
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import produce from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/jobs/components/table/event_hub';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
@@ -42,7 +42,7 @@ export default {
this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {};
},
error() {
- createFlash({ message: __('An error occurred while fetching the pipelines jobs.') });
+ createAlert({ message: __('An error occurred while fetching the pipelines jobs.') });
},
},
},
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
index ca2537ca4f4..7ee5ec48f44 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
@@ -1,6 +1,6 @@
<script>
import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { dasherize } from '~/lib/utils/text_utility';
@@ -81,7 +81,7 @@ export default {
reportToSentry('action_component', err);
- createFlash({
+ createAlert({
message: __('An error occurred while making the request.'),
});
});
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
index a68797a7235..f1c6c6633eb 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
@@ -14,7 +14,7 @@
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import eventHub from '../../event_hub';
@@ -94,7 +94,7 @@ export default {
this.$refs.dropdown.hide();
this.isLoading = false;
- createFlash({
+ createAlert({
message: __('Something went wrong on our end.'),
});
});
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
index df59962569e..2a78636261b 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
@@ -56,7 +56,12 @@ export default {
<template>
<gl-tabs>
- <gl-tab ref="pipelineTab" :title="$options.i18n.tabs.pipelineTitle" data-testid="pipeline-tab">
+ <gl-tab
+ ref="pipelineTab"
+ :title="$options.i18n.tabs.pipelineTitle"
+ data-testid="pipeline-tab"
+ lazy
+ >
<pipeline-graph-wrapper />
</gl-tab>
<gl-tab
@@ -64,6 +69,7 @@ export default {
:title="$options.i18n.tabs.needsTitle"
:active="isActive($options.tabNames.needs)"
data-testid="dag-tab"
+ lazy
>
<dag />
</gl-tab>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
index 2d2f649f651..73a255f392b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -55,6 +55,9 @@ export default {
};
},
computed: {
+ hasArtifacts() {
+ return this.artifacts.length > 0;
+ },
filteredArtifacts() {
return this.searchQuery.length > 0
? fuzzaldrinPlus.filter(this.artifacts, this.searchQuery, { key: 'name' })
@@ -86,7 +89,9 @@ export default {
});
},
handleDropdownShown() {
- this.$refs.searchInput.focusInput();
+ if (this.hasArtifacts) {
+ this.$refs.searchInput.focusInput();
+ }
},
},
};
@@ -112,12 +117,12 @@ export default {
<gl-loading-icon v-else-if="isLoading" size="sm" />
- <gl-dropdown-item v-else-if="!artifacts.length" data-testid="artifacts-empty-message">
+ <gl-dropdown-item v-else-if="!hasArtifacts" data-testid="artifacts-empty-message">
{{ $options.i18n.emptyArtifactsMessage }}
</gl-dropdown-item>
<template #header>
- <gl-search-box-by-type v-if="artifacts.length" ref="searchInput" v-model.trim="searchQuery" />
+ <gl-search-box-by-type v-if="hasArtifacts" ref="searchInput" v-model.trim="searchQuery" />
</template>
<gl-dropdown-item
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index f9022be888a..30528ce8d17 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/flash';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -249,7 +249,7 @@ export default {
this.updateContent(params);
- this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs });
+ this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs, property: scope });
},
successCallback(resp) {
// Because we are polling & the user is interacting verify if the response received
@@ -267,14 +267,14 @@ export default {
.postAction(endpoint)
.then(() => {
this.isResetCacheButtonLoading = false;
- createFlash({
+ createAlert({
message: s__('Pipelines|Project cache successfully reset.'),
- type: 'notice',
+ variant: VARIANT_INFO,
});
})
.catch(() => {
this.isResetCacheButtonLoading = false;
- createFlash({
+ createAlert({
message: s__('Pipelines|Something went wrong while cleaning runners cache.'),
});
});
@@ -301,9 +301,9 @@ export default {
}
if (!filter.type) {
- createFlash({
+ createAlert({
message: RAW_TEXT_WARNING,
- type: 'warning',
+ variant: VARIANT_WARNING,
});
}
});
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
index 16a747f6165..f34b3f56c5b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { s__, __, sprintf } from '~/locale';
@@ -66,7 +66,7 @@ export default {
})
.catch(() => {
this.isLoading = false;
- createFlash({ message: __('An error occurred while making the request.') });
+ createAlert({ message: __('An error occurred while making the request.') });
});
},
isActionDisabled(action) {
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 1db2898b72a..b57d0ac1fd7 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -2,7 +2,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
@@ -45,7 +45,7 @@ export default {
this.loading = false;
})
.catch((err) => {
- createFlash({
+ createAlert({
message: FETCH_BRANCH_ERROR_MESSAGE,
});
this.loading = false;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
index afcdd63b664..5846a1f6ed9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
@@ -2,7 +2,7 @@
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
export default {
@@ -38,7 +38,7 @@ export default {
this.loading = false;
})
.catch((err) => {
- createFlash({
+ createAlert({
message: FETCH_TAG_ERROR_MESSAGE,
});
this.loading = false;
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
index 746cf238646..73f7d3f52c3 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
@@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
ANY_TRIGGER_AUTHOR,
FETCH_AUTHOR_ERROR_MESSAGE,
@@ -61,7 +61,7 @@ export default {
this.loading = false;
})
.catch((err) => {
- createFlash({
+ createAlert({
message: FETCH_AUTHOR_ERROR_MESSAGE,
});
this.loading = false;
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index e8e49cc652e..9602ca1ba88 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import createFlash, { createAlert } from '~/flash';
+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';
@@ -172,7 +172,7 @@ export default {
.postAction(endpoint)
.then(() => this.updateTable())
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while making the request.'),
}),
);
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 8bdf18da348..3744649e9d5 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__ } from '~/locale';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
@@ -24,7 +24,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading a section of this page.'),
});
}
@@ -37,7 +37,7 @@ export default async function initPipelineDetailsBundle() {
const appOptions = createAppOptions(SELECTORS.PIPELINE_TABS, apolloProvider);
createPipelineTabs(appOptions);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading a section of this page.'),
});
}
@@ -45,7 +45,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the pipeline.'),
});
}
@@ -53,7 +53,7 @@ export default async function initPipelineDetailsBundle() {
try {
createDagApp(apolloProvider);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the Needs tab.'),
});
}
@@ -61,7 +61,7 @@ export default async function initPipelineDetailsBundle() {
try {
createTestDetails(SELECTORS.PIPELINE_TESTS);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the Test Reports tab.'),
});
}
@@ -69,7 +69,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
} catch {
- createFlash({
+ createAlert({
message: __('An error occurred while loading the Jobs tab.'),
});
}
@@ -77,7 +77,7 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
} catch {
- createFlash({
+ createAlert({
message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
});
}
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index b785fd1753c..c77b4813e33 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -12,7 +12,7 @@ export const fetchSummary = ({ state, commit, dispatch }) => {
commit(types.SET_SUMMARY, data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__('TestReports|There was an error fetching the summary.'),
});
})
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index 68ee063dda7..bff30acfe36 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -21,7 +21,7 @@ export default {
if (errorMessage) {
state.errorMessage = errorMessage;
} else {
- createFlash({
+ createAlert({
message: s__('TestReports|There was an error fetching the test suite.'),
});
}
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index c99133fd251..b038b78088f 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, s__, sprintf } from '~/locale';
@@ -85,12 +85,12 @@ Please update your Git repository remotes as soon as possible.`),
return axios
.put(this.actionUrl, putData)
.then((result) => {
- createFlash({ message: result.data.message, type: 'notice' });
+ createAlert({ message: result.data.message, variant: VARIANT_INFO });
this.username = username;
this.isRequestPending = false;
})
.catch((error) => {
- createFlash({
+ createAlert({
message:
error?.response?.data?.message ||
s__('Profiles|An error occurred while updating your username, please try again.'),
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index 722f7d467a2..050b004f657 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -165,6 +165,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
setPreview() {
const filename = this.fileInput.val().replace(FILENAMEREGEX, '');
this.previewImage.attr('src', this.dataURL);
+ this.previewImage.attr('srcset', this.dataURL);
return this.filename.text(filename);
}
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 7542f81a361..a33a20b49f6 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/flash';
import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
@@ -94,15 +94,15 @@ export default {
return;
}
updateClasses(this.bodyClasses, this.getSelectedTheme().css_class, this.selectedLayout);
- const { message = this.$options.i18n.defaultSuccess, type = FLASH_TYPES.NOTICE } =
+ const { message = this.$options.i18n.defaultSuccess, variant = VARIANT_INFO } =
customEvent?.detail?.[0] || {};
- createFlash({ message, type });
+ createAlert({ message, variant });
this.isSubmitEnabled = true;
},
handleError(customEvent) {
- const { message = this.$options.i18n.defaultError, type = FLASH_TYPES.ALERT } =
+ const { message = this.$options.i18n.defaultError, variant = VARIANT_DANGER } =
customEvent?.detail?.[0] || {};
- createFlash({ message, type });
+ createAlert({ message, variant });
this.isSubmitEnabled = true;
},
},
@@ -110,7 +110,7 @@ export default {
</script>
<template>
- <div class="row gl-mt-3 js-preferences-form">
+ <div class="row gl-mt-3 js-preferences-form js-search-settings-section">
<div v-if="integrationViews.length" class="col-sm-12">
<hr data-testid="profile-preferences-integrations-rule" />
</div>
@@ -131,9 +131,9 @@ export default {
:message-url="view.message_url"
:config="$options.integrationViewConfigs[view.name]"
/>
- </div>
- <div class="col-sm-12">
<hr />
+ </div>
+ <div class="col-sm-12 js-hide-when-nothing-matches-search">
<gl-button
category="primary"
variant="confirm"
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index af5beeb686c..93bc203d391 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -5,9 +5,6 @@ import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { Rails } from '~/lib/utils/rails_ujs';
-import TimezoneDropdown, {
- formatTimezone,
-} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
export default class Profile {
@@ -17,21 +14,12 @@ export default class Profile {
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
-
- this.$inputEl = $('#user_timezone');
-
- this.timezoneDropdown = new TimezoneDropdown({
- $inputEl: this.$inputEl,
- $dropdownEl: $('.js-timezone-dropdown'),
- displayFormat: (selectedItem) => formatTimezone(selectedItem),
- allowEmpty: true,
- });
}
initAvatarGlCrop() {
const cropOpts = {
filename: '.js-avatar-filename',
- previewImage: '.avatar-image .avatar',
+ previewImage: '.avatar-image .gl-avatar',
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 09acf98001c..705234537a8 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Api from './api';
import { loadCSSFile } from './lib/utils/css_utils';
import { s__ } from './locale';
@@ -67,7 +67,7 @@ const projectSelect = async () => {
},
projectsCallback,
).catch(() => {
- createFlash({
+ createAlert({
message: s__('ProjectSelect|Something went wrong while fetching projects'),
});
});
diff --git a/app/assets/javascripts/projects/commit/store/actions.js b/app/assets/javascripts/projects/commit/store/actions.js
index 2b25082eced..cfff93eac5a 100644
--- a/app/assets/javascripts/projects/commit/store/actions.js
+++ b/app/assets/javascripts/projects/commit/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { PROJECT_BRANCHES_ERROR } from '../constants';
import * as types from './mutation_types';
@@ -26,7 +26,7 @@ export const fetchBranches = ({ commit, dispatch, state }, query) => {
commit(types.RECEIVE_BRANCHES_SUCCESS, data.Branches?.length ? data.Branches : data);
})
.catch(() => {
- createFlash({ message: PROJECT_BRANCHES_ERROR });
+ createAlert({ message: PROJECT_BRANCHES_ERROR });
});
};
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 4505dd1f85c..2802e4a90b9 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
getQueryHeaders,
@@ -59,7 +59,7 @@ export default {
return project?.pipeline;
},
error() {
- createFlash({ message: this.$options.i18n.linkedPipelinesFetchError });
+ createAlert({ message: this.$options.i18n.linkedPipelinesFetchError });
},
},
pipelineStages: {
@@ -78,7 +78,7 @@ export default {
return project?.pipeline?.stages?.nodes || [];
},
error() {
- createFlash({ message: this.$options.i18n.stagesFetchError });
+ createAlert({ message: this.$options.i18n.stagesFetchError });
},
},
},
@@ -108,7 +108,7 @@ export default {
try {
this.formattedStages = formatStages(this.pipelineStages, this.stages);
} catch (error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.stageConversionError,
captureError: true,
error,
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index 5a9d3129809..62b1209131c 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
getQueryHeaders,
toggleQueryPollingByVisibility,
@@ -44,7 +44,7 @@ export default {
return project?.pipeline?.detailedStatus || {};
},
error() {
- createFlash({ message: this.$options.PIPELINE_STATUS_FETCH_ERROR });
+ createAlert({ message: this.$options.PIPELINE_STATUS_FETCH_ERROR });
},
},
},
diff --git a/app/assets/javascripts/projects/commits/store/actions.js b/app/assets/javascripts/projects/commits/store/actions.js
index 795c293d14b..603fdfdf80a 100644
--- a/app/assets/javascripts/projects/commits/store/actions.js
+++ b/app/assets/javascripts/projects/commits/store/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -13,7 +13,7 @@ export default {
commit(types.COMMITS_AUTHORS, authors);
},
receiveAuthorsError() {
- createFlash({
+ createAlert({
message: __('An error occurred fetching the project authors.'),
});
},
diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue
index 4ba7156b026..271694863e8 100644
--- a/app/assets/javascripts/projects/compare/components/app.vue
+++ b/app/assets/javascripts/projects/compare/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { joinPaths } from '~/lib/utils/url_utility';
import RevisionCard from './revision_card.vue';
@@ -9,6 +9,8 @@ export default {
components: {
RevisionCard,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
props: {
projectCompareIndexPath: {
@@ -53,6 +55,10 @@ export default {
type: Array,
required: true,
},
+ straight: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
@@ -67,8 +73,27 @@ export default {
revision: this.paramsTo,
refsProjectPath: this.sourceProjectRefsPath,
},
+ isStraight: this.straight,
};
},
+ computed: {
+ straightModeDropdownItems() {
+ return [
+ {
+ modeType: 'off',
+ isEnabled: false,
+ content: '..',
+ testId: 'disableStraightModeButton',
+ },
+ {
+ modeType: 'on',
+ isEnabled: true,
+ content: '...',
+ testId: 'enableStraightModeButton',
+ },
+ ];
+ },
+ },
methods: {
onSubmit() {
this.$refs.form.submit();
@@ -85,6 +110,9 @@ export default {
onSwapRevision() {
[this.from, this.to] = [this.to, this.from]; // swaps 'from' and 'to'
},
+ setStraightMode(isStraight) {
+ this.isStraight = isStraight;
+ },
},
};
</script>
@@ -112,10 +140,22 @@ export default {
@selectRevision="onSelectRevision"
/>
<div
- class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-4 gl-md-my-0"
+ class="gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-3 gl-md-my-0 gl-pl-3 gl-pr-3"
data-testid="ellipsis"
>
- ...
+ <input :value="isStraight ? 'true' : 'false'" type="hidden" name="straight" />
+ <gl-dropdown data-testid="modeDropdown" :text="isStraight ? '...' : '..'" size="small">
+ <gl-dropdown-item
+ v-for="mode in straightModeDropdownItems"
+ :key="mode.modeType"
+ :is-check-item="true"
+ :is-checked="isStraight == mode.isEnabled"
+ :data-testid="mode.testId"
+ @click="setStraightMode(mode.isEnabled)"
+ >
+ <span class="dropdown-menu-inner-content"> {{ mode.content }} </span>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
<revision-card
data-testid="targetRevisionCard"
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
index f0b8e73e528..10531e950f9 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
import { debounce } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
@@ -76,7 +76,7 @@ export default {
this.tags = data.Tags || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__(
'CompareRevisions|There was an error while searching the branch/tag list. Please try again.',
),
@@ -97,7 +97,7 @@ export default {
this.tags = data.Tags || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__(
'CompareRevisions|There was an error while loading the branch/tag list. Please try again.',
),
diff --git a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
index 19cf4cda2be..1e1677e947c 100644
--- a/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_dropdown_legacy.vue
@@ -1,6 +1,6 @@
<script>
import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlDropdownSectionHeader } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
@@ -74,7 +74,7 @@ export default {
this.tags = data.Tags || [];
})
.catch(() => {
- createFlash({
+ createAlert({
message: `${s__(
'CompareRevisions|There was an error while updating the branch/tag list. Please try again.',
)}`,
diff --git a/app/assets/javascripts/projects/compare/index.js b/app/assets/javascripts/projects/compare/index.js
index 074b8565c3c..284cee6d7f1 100644
--- a/app/assets/javascripts/projects/compare/index.js
+++ b/app/assets/javascripts/projects/compare/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import CompareApp from './components/app.vue';
export default function init() {
@@ -9,6 +10,7 @@ export default function init() {
targetProjectRefsPath,
paramsFrom,
paramsTo,
+ straight,
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
@@ -29,6 +31,7 @@ export default function init() {
targetProjectRefsPath,
paramsFrom,
paramsTo,
+ straight: parseBoolean(straight),
projectCompareIndexPath,
projectMergeRequestPath,
createMrPath,
diff --git a/app/assets/javascripts/projects/project_find_file.js b/app/assets/javascripts/projects/project_find_file.js
index d295c06928f..71329c4f461 100644
--- a/app/assets/javascripts/projects/project_find_file.js
+++ b/app/assets/javascripts/projects/project_find_file.js
@@ -2,7 +2,7 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -89,7 +89,7 @@ export default class ProjectFindFile {
this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus();
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while loading filenames'),
}),
);
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 060178a3cfb..335545c802a 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import { escape, find, countBy } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { n__, s__, __, sprintf } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
@@ -326,12 +326,12 @@ export default class AccessDropdown {
);
})
.catch(() => {
- createFlash({ message: __('Failed to load groups, users and deploy keys.') });
+ createAlert({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
getDeployKeys(query)
.then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createFlash({ message: __('Failed to load deploy keys.') }));
+ .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
}
}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue
index 6ba2ef7da99..f2b1c749abc 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/branch_dropdown.vue
@@ -10,7 +10,7 @@ import {
import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
-import branchesQuery from '../queries/branches.query.graphql';
+import branchesQuery from '../../queries/branches.query.graphql';
export const i18n = {
fetchBranchesError: s__('BranchRules|An error occurred while fetching branches.'),
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue
index ad3eb7d2899..ad3eb7d2899 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/index.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue
index bcc0f64d667..bcc0f64d667 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/index.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue
index 85f168af4a8..85f168af4a8 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/merge_protections.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/merge_protections.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue
index 541923bb735..541923bb735 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/protections/push_protections.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
new file mode 100644
index 00000000000..264c2629433
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
@@ -0,0 +1,42 @@
+import { s__ } from '~/locale';
+
+export const I18N = {
+ manageProtectionsLinkTitle: s__('BranchRules|Manage in Protected Branches'),
+ targetBranch: s__('BranchRules|Target Branch'),
+ branchNameOrPattern: s__('BranchRules|Branch name or pattern'),
+ branch: s__('BranchRules|Target Branch'),
+ allBranches: s__('BranchRules|All branches'),
+ protectBranchTitle: s__('BranchRules|Protect branch'),
+ protectBranchDescription: s__(
+ 'BranchRules|Keep stable branches secure and force developers to use merge requests. %{linkStart}What are protected branches?%{linkEnd}',
+ ),
+ wildcardsHelpText: s__(
+ 'BranchRules|%{linkStart}Wildcards%{linkEnd} such as *-stable or production/ are supported',
+ ),
+ forcePushTitle: s__('BranchRules|Force push'),
+ allowForcePushDescription: s__(
+ 'BranchRules|All users with push access are allowed to force push.',
+ ),
+ disallowForcePushDescription: s__('BranchRules|Force push is not allowed.'),
+ approvalsTitle: s__('BranchRules|Approvals'),
+ manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'),
+ approvalsDescription: s__(
+ 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Lean more.%{linkEnd}',
+ ),
+ statusChecksTitle: s__('BranchRules|Status checks'),
+ allowedToPushHeader: s__('BranchRules|Allowed to push (%{total})'),
+ allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'),
+ approvalsHeader: s__('BranchRules|Required approvals (%{total})'),
+ noData: s__('BranchRules|No data to display'),
+};
+
+export const BRANCH_PARAM_NAME = 'branch';
+
+export const ALL_BRANCHES_WILDCARD = '*';
+
+export const WILDCARDS_HELP_PATH =
+ 'user/project/protected_branches#configure-multiple-protected-branches-by-using-a-wildcard';
+
+export const PROTECTED_BRANCHES_HELP_PATH = 'user/project/protected_branches';
+
+export const APPROVALS_HELP_PATH = 'user/project/merge_requests/approvals/index.md';
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
new file mode 100644
index 00000000000..318940478a8
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -0,0 +1,207 @@
+<script>
+import { GlSprintf, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import branchRulesQuery from '../../queries/branch_rules_details.query.graphql';
+import Protection from './protection.vue';
+import {
+ I18N,
+ ALL_BRANCHES_WILDCARD,
+ BRANCH_PARAM_NAME,
+ WILDCARDS_HELP_PATH,
+ PROTECTED_BRANCHES_HELP_PATH,
+ APPROVALS_HELP_PATH,
+} from './constants';
+
+const wildcardsHelpDocLink = helpPagePath(WILDCARDS_HELP_PATH);
+const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH);
+const approvalsHelpDocLink = helpPagePath(APPROVALS_HELP_PATH);
+
+export default {
+ name: 'RuleView',
+ i18n: I18N,
+ wildcardsHelpDocLink,
+ protectedBranchesHelpDocLink,
+ approvalsHelpDocLink,
+ components: { Protection, GlSprintf, GlLink, GlLoadingIcon },
+ inject: {
+ projectPath: {
+ default: '',
+ },
+ protectedBranchesPath: {
+ default: '',
+ },
+ approvalRulesPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ project: {
+ query: branchRulesQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update({ project: { branchRules } }) {
+ this.branchProtection = branchRules.nodes.find(
+ (rule) => rule.name === this.branch,
+ )?.branchProtection;
+ },
+ },
+ },
+ data() {
+ return {
+ branch: getParameterByName(BRANCH_PARAM_NAME),
+ branchProtection: {
+ approvalRules: {},
+ },
+ };
+ },
+ computed: {
+ forcePushDescription() {
+ return this.branchProtection?.allowForcePush
+ ? this.$options.i18n.allowForcePushDescription
+ : this.$options.i18n.disallowForcePushDescription;
+ },
+ mergeAccessLevels() {
+ const { mergeAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(mergeAccessLevels);
+ },
+ pushAccessLevels() {
+ const { pushAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(pushAccessLevels);
+ },
+ allowedToMergeHeader() {
+ return sprintf(this.$options.i18n.allowedToMergeHeader, {
+ total: this.mergeAccessLevels.total,
+ });
+ },
+ allowedToPushHeader() {
+ return sprintf(this.$options.i18n.allowedToPushHeader, {
+ total: this.pushAccessLevels.total,
+ });
+ },
+ approvalsHeader() {
+ const total = this.approvals.reduce(
+ (sum, { approvalsRequired }) => sum + approvalsRequired,
+ 0,
+ );
+ return sprintf(this.$options.i18n.approvalsHeader, {
+ total,
+ });
+ },
+ allBranches() {
+ return this.branch === ALL_BRANCHES_WILDCARD;
+ },
+ allBranchesLabel() {
+ return this.$options.i18n.allBranches;
+ },
+ branchTitle() {
+ return this.allBranches
+ ? this.$options.i18n.targetBranch
+ : this.$options.i18n.branchNameOrPattern;
+ },
+ approvals() {
+ return this.branchProtection?.approvalRules?.nodes || [];
+ },
+ },
+ methods: {
+ getAccessLevels(accessLevels = {}) {
+ const total = accessLevels.edges?.length;
+ const accessLevelTypes = { total, users: [], groups: [], roles: [] };
+
+ accessLevels.edges?.forEach(({ node }) => {
+ if (node.user) {
+ const src = node.user.avatarUrl;
+ accessLevelTypes.users.push({ src, ...node.user });
+ } else if (node.group) {
+ accessLevelTypes.groups.push(node);
+ } else {
+ accessLevelTypes.roles.push(node);
+ }
+ });
+
+ return accessLevelTypes;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-loading-icon v-if="$apollo.loading" />
+ <div v-else-if="!branchProtection">{{ $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">
+ <gl-sprintf :message="$options.i18n.wildcardsHelpText">
+ <template #link="{ content }">
+ <gl-link :href="$options.wildcardsHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+
+ <div v-if="allBranches" class="gl-mt-2" data-testid="branch">
+ {{ allBranchesLabel }}
+ </div>
+ <code v-else class="gl-mt-2" data-testid="branch">{{ branch }}</code>
+
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.protectBranchTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.protectBranchDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.protectedBranchesHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <!-- Allowed to push -->
+ <protection
+ class="gl-mt-3"
+ :header="allowedToPushHeader"
+ :header-link-title="$options.i18n.manageProtectionsLinkTitle"
+ :header-link-href="protectedBranchesPath"
+ :roles="pushAccessLevels.roles"
+ :users="pushAccessLevels.users"
+ :groups="pushAccessLevels.groups"
+ />
+
+ <!-- Force push -->
+ <strong>{{ $options.i18n.forcePushTitle }}</strong>
+ <p>{{ forcePushDescription }}</p>
+
+ <!-- Allowed to merge -->
+ <protection
+ :header="allowedToMergeHeader"
+ :header-link-title="$options.i18n.manageProtectionsLinkTitle"
+ :header-link-href="protectedBranchesPath"
+ :roles="mergeAccessLevels.roles"
+ :users="mergeAccessLevels.users"
+ :groups="mergeAccessLevels.groups"
+ />
+
+ <!-- Approvals -->
+ <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.approvalsTitle }}</h4>
+ <gl-sprintf :message="$options.i18n.approvalsDescription">
+ <template #link="{ content }">
+ <gl-link :href="$options.approvalsHelpDocLink">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+
+ <protection
+ class="gl-mt-3"
+ :header="approvalsHeader"
+ :header-link-title="$options.i18n.manageApprovalsLinkTitle"
+ :header-link-href="approvalRulesPath"
+ :approvals="approvals"
+ />
+
+ <!-- Status checks -->
+ <!-- Follow-up: add status checks section (https://gitlab.com/gitlab-org/gitlab/-/issues/372362) -->
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
new file mode 100644
index 00000000000..cfe2df0dbda
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection.vue
@@ -0,0 +1,99 @@
+<script>
+import { GlCard, GlLink } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ProtectionRow from './protection_row.vue';
+
+export const i18n = {
+ rolesTitle: s__('BranchRules|Roles'),
+ usersTitle: s__('BranchRules|Users'),
+ groupsTitle: s__('BranchRules|Groups'),
+};
+
+export default {
+ name: 'ProtectionDetail',
+ i18n,
+ components: { GlCard, GlLink, ProtectionRow },
+ props: {
+ header: {
+ type: String,
+ required: true,
+ },
+ headerLinkTitle: {
+ type: String,
+ required: true,
+ },
+ headerLinkHref: {
+ type: String,
+ required: true,
+ },
+ roles: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ groups: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvals: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ showUsersDivider() {
+ return Boolean(this.roles.length);
+ },
+ showGroupsDivider() {
+ return Boolean(this.roles.length || this.users.length);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card class="gl-mb-5" body-class="gl-py-0">
+ <template #header>
+ <div class="gl-display-flex gl-justify-content-space-between">
+ <strong>{{ header }}</strong>
+ <gl-link :href="headerLinkHref">{{ headerLinkTitle }}</gl-link>
+ </div>
+ </template>
+
+ <!-- Roles -->
+ <protection-row v-if="roles.length" :title="$options.i18n.rolesTitle" :access-levels="roles" />
+
+ <!-- Users -->
+ <protection-row
+ v-if="users.length"
+ :show-divider="showUsersDivider"
+ :users="users"
+ :title="$options.i18n.usersTitle"
+ />
+
+ <!-- Groups -->
+ <protection-row
+ v-if="groups.length"
+ :show-divider="showGroupsDivider"
+ :title="$options.i18n.groupsTitle"
+ :access-levels="groups"
+ />
+
+ <!-- Approvals -->
+ <protection-row
+ v-for="(approval, index) in approvals"
+ :key="approval.name"
+ :show-divider="index !== 0"
+ :title="approval.name"
+ :users="approval.eligibleApprovers.nodes"
+ :approvals-required="approval.approvalsRequired"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
new file mode 100644
index 00000000000..28a1c09fa82
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlAvatarsInline, GlAvatar, GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+import { n__ } from '~/locale';
+
+const AVATAR_TOOLTIP_MAX_CHARS = 100;
+export const MAX_VISIBLE_AVATARS = 4;
+export const AVATAR_SIZE = 32;
+
+export default {
+ name: 'ProtectionRow',
+ AVATAR_TOOLTIP_MAX_CHARS,
+ MAX_VISIBLE_AVATARS,
+ AVATAR_SIZE,
+ components: { GlAvatarsInline, GlAvatar, GlAvatarLink },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ accessLevels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ showDivider: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ approvalsRequired: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ avatarBadgeSrOnlyText() {
+ return n__(
+ '%d additional user',
+ '%d additional users',
+ this.users.length - this.$options.MAX_VISIBLE_AVATARS,
+ );
+ },
+ commaSeparateList() {
+ return this.accessLevels.length > 1;
+ },
+ approvalsRequiredTitle() {
+ return this.approvalsRequired
+ ? n__('%d approval required', '%d approvals required', this.approvalsRequired)
+ : null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4"
+ :class="{ 'gl-border-t-solid': showDivider }"
+ >
+ <div class="gl-display-flex gl-w-half gl-justify-content-space-between">
+ <div class="gl-mr-7 gl-w-quarter">{{ title }}</div>
+
+ <gl-avatars-inline
+ v-if="users.length"
+ class="gl-w-quarter!"
+ :avatars="users"
+ :collapsed="true"
+ :max-visible="$options.MAX_VISIBLE_AVATARS"
+ :avatar-size="$options.AVATAR_SIZE"
+ badge-tooltip-prop="name"
+ :badge-tooltip-max-chars="$options.AVATAR_TOOLTIP_MAX_CHARS"
+ :badge-sr-only-text="avatarBadgeSrOnlyText"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link
+ :key="avatar.username"
+ v-gl-tooltip
+ target="_blank"
+ :href="avatar.webUrl"
+ :title="avatar.name"
+ >
+ <gl-avatar :src="avatar.avatarUrl" :label="avatar.name" :size="$options.AVATAR_SIZE" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+
+ <div
+ v-for="(item, index) in accessLevels"
+ :key="index"
+ data-testid="access-level"
+ class="gl-w-quarter"
+ >
+ <span v-if="commaSeparateList && index > 0" data-testid="comma-separator">,</span>
+ {{ item.accessLevelDescription }}
+ </div>
+
+ <div class="gl-ml-7 gl-w-quarter">{{ approvalsRequiredTitle }}</div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
index 8452542540e..07fd0a7080f 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import RuleEdit from './components/rule_edit.vue';
+import View from './components/view/index.vue';
export default function mountBranchRules(el) {
if (!el) {
@@ -14,13 +14,18 @@ export default function mountBranchRules(el) {
defaultClient: createDefaultClient(),
});
- const { projectPath } = el.dataset;
+ const { projectPath, protectedBranchesPath, approvalRulesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
+ provide: {
+ projectPath,
+ protectedBranchesPath,
+ approvalRulesPath,
+ },
render(h) {
- return h(RuleEdit, { props: { projectPath } });
+ return h(View);
},
});
}
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
new file mode 100644
index 00000000000..3ac165498a1
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -0,0 +1,50 @@
+query getBranchRulesDetails($projectPath: ID!) {
+ project(fullPath: $projectPath) {
+ id
+ branchRules {
+ nodes {
+ name
+ branchProtection {
+ allowForcePush
+ codeOwnerApprovalRequired
+ mergeAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ group {
+ id
+ avatarUrl
+ }
+ user {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ pushAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ group {
+ id
+ avatarUrl
+ }
+ user {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index 2209172c06d..cc47496971d 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -9,7 +9,7 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
@@ -163,7 +163,7 @@ export default {
this.setSelected({ initial });
})
.catch(() =>
- createFlash({ message: __('Failed to load groups, users and deploy keys.') }),
+ createAlert({ message: __('Failed to load groups, users and deploy keys.') }),
)
.finally(() => {
this.initialLoading = false;
@@ -175,7 +175,7 @@ export default {
this.consolidateData(deployKeysResponse.data);
this.setSelected({ initial });
})
- .catch(() => createFlash({ message: __('Failed to load deploy keys.') }))
+ .catch(() => createAlert({ message: __('Failed to load deploy keys.') }))
.finally(() => {
this.initialLoading = false;
this.loading = false;
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
new file mode 100644
index 00000000000..fee2f591216
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -0,0 +1,38 @@
+<script>
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES } from '~/ref/constants';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ RefSelector,
+ },
+ props: {
+ persistedDefaultBranch: {
+ type: String,
+ required: true,
+ },
+ projectId: {
+ type: String,
+ required: true,
+ },
+ },
+ refTypes: [REF_TYPE_BRANCHES],
+ i18n: {
+ dropdownHeader: __('Select default branch'),
+ searchPlaceholder: __('Search branch'),
+ },
+};
+</script>
+<template>
+ <ref-selector
+ :value="persistedDefaultBranch"
+ class="gl-w-full"
+ :project-id="projectId"
+ :enabled-ref-types="$options.refTypes"
+ :translations="$options.i18n"
+ name="project[default_branch]"
+ data-testid="default-branch-dropdown"
+ data-qa-selector="default_branch_dropdown"
+ />
+</template>
diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
index c13753da00b..55420c9c732 100644
--- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
+++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue
@@ -1,13 +1,14 @@
<script>
-import { GlFormGroup } from '@gitlab/ui';
-import produce from 'immer';
+import { GlFormGroup, GlAlert } from '@gitlab/ui';
+import { debounce } from 'lodash';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
+import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { getTransferLocations } from '~/api/projects_api';
+import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanTransferProjects from '../graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql';
-
-const GROUPS_PER_PAGE = 25;
+import { s__, __ } from '~/locale';
+import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql';
export default {
name: 'TransferProjectForm',
@@ -15,7 +16,15 @@ export default {
GlFormGroup,
NamespaceSelect,
ConfirmDanger,
+ GlAlert,
+ },
+ i18n: {
+ errorMessage: s__(
+ 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.',
+ ),
+ alertDismissAlert: __('Dismiss'),
},
+ inject: ['projectId'],
props: {
confirmationPhrase: {
type: String,
@@ -26,93 +35,131 @@ export default {
required: true,
},
},
- apollo: {
- currentUser: {
- query: searchNamespacesWhereUserCanTransferProjects,
- debounce: DEBOUNCE_DELAY,
- variables() {
- return {
- search: this.searchTerm,
- after: null,
- first: GROUPS_PER_PAGE,
- };
- },
- result() {
- this.isLoadingMoreGroups = false;
- this.isSearchLoading = false;
- },
- },
- },
data() {
return {
- currentUser: {},
+ userNamespaces: [],
+ groupNamespaces: [],
+ initialNamespacesLoaded: false,
selectedNamespace: null,
- isLoadingMoreGroups: false,
+ hasError: false,
+ isLoading: false,
isSearchLoading: false,
searchTerm: '',
+ page: 1,
+ totalPages: 1,
};
},
computed: {
hasSelectedNamespace() {
return Boolean(this.selectedNamespace?.id);
},
- groupNamespaces() {
- return this.currentUser.groups?.nodes?.map(this.formatNamespace) || [];
- },
- userNamespaces() {
- const { namespace } = this.currentUser;
-
- return namespace ? [this.formatNamespace(namespace)] : [];
- },
hasNextPageOfGroups() {
- return this.currentUser.groups?.pageInfo?.hasNextPage || false;
+ return this.page < this.totalPages;
},
},
methods: {
+ async handleShow() {
+ if (this.initialNamespacesLoaded) {
+ return;
+ }
+
+ this.isLoading = true;
+
+ [this.groupNamespaces, this.userNamespaces] = await Promise.all([
+ this.getGroupNamespaces(),
+ this.getUserNamespaces(),
+ ]);
+
+ this.isLoading = false;
+ this.initialNamespacesLoaded = true;
+ },
handleSelect(selectedNamespace) {
this.selectedNamespace = selectedNamespace;
this.$emit('selectNamespace', selectedNamespace.id);
},
- handleLoadMoreGroups() {
- this.isLoadingMoreGroups = true;
+ async getGroupNamespaces() {
+ try {
+ const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, {
+ page: this.page,
+ search: this.searchTerm,
+ });
+
+ const { totalPages } = parseIntPagination(normalizeHeaders(headers));
+ this.totalPages = totalPages;
- this.$apollo.queries.currentUser.fetchMore({
- variables: {
- after: this.currentUser.groups.pageInfo.endCursor,
- first: GROUPS_PER_PAGE,
- },
- updateQuery(
- previousResult,
+ return groupNamespaces.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }));
+ } catch (error) {
+ this.hasError = true;
+
+ return [];
+ }
+ },
+ async getUserNamespaces() {
+ try {
+ const {
+ data: {
+ currentUser: { namespace },
+ },
+ } = await this.$apollo.query({
+ query: currentUserNamespace,
+ });
+
+ if (!namespace) {
+ return [];
+ }
+
+ return [
{
- fetchMoreResult: {
- currentUser: { groups: newGroups },
- },
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
},
- ) {
- const previousGroups = previousResult.currentUser.groups;
+ ];
+ } catch (error) {
+ this.hasError = true;
- return produce(previousResult, (draftData) => {
- draftData.currentUser.groups.nodes = [...previousGroups.nodes, ...newGroups.nodes];
- draftData.currentUser.groups.pageInfo = newGroups.pageInfo;
- });
- },
- });
+ return [];
+ }
},
- handleSearch(searchTerm) {
+ async handleLoadMoreGroups() {
+ this.isLoading = true;
+ this.page += 1;
+
+ const groupNamespaces = await this.getGroupNamespaces();
+ this.groupNamespaces.push(...groupNamespaces);
+
+ this.isLoading = false;
+ },
+ debouncedSearch: debounce(async function debouncedSearch() {
this.isSearchLoading = true;
+
+ this.groupNamespaces = await this.getGroupNamespaces();
+
+ this.isSearchLoading = false;
+ }, DEBOUNCE_DELAY),
+ handleSearch(searchTerm) {
this.searchTerm = searchTerm;
+ this.page = 1;
+
+ this.debouncedSearch();
},
- formatNamespace({ id, fullName }) {
- return {
- id: getIdFromGraphQLId(id),
- humanName: fullName,
- };
+ handleAlertDismiss() {
+ this.hasError = false;
},
},
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="hasError"
+ variant="danger"
+ :dismiss-label="$options.i18n.alertDismissLabel"
+ @dismiss="handleAlertDismiss"
+ >{{ $options.i18n.errorMessage }}</gl-alert
+ >
<gl-form-group>
<namespace-select
data-testid="transfer-project-namespace"
@@ -121,12 +168,13 @@ export default {
:user-namespaces="userNamespaces"
:selected-namespace="selectedNamespace"
:has-next-page-of-groups="hasNextPageOfGroups"
- :is-loading-more-groups="isLoadingMoreGroups"
+ :is-loading="isLoading"
:is-search-loading="isSearchLoading"
:should-filter-namespaces="false"
@select="handleSelect"
@load-more-groups="handleLoadMoreGroups"
@search="handleSearch"
+ @show="handleShow"
/>
</gl-form-group>
<confirm-danger
diff --git a/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql
new file mode 100644
index 00000000000..7ae6ee1428b
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/graphql/queries/current_user_namespace.query.graphql
@@ -0,0 +1,9 @@
+query currentUserNamespace {
+ currentUser {
+ id
+ namespace {
+ id
+ fullName
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql b/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql
deleted file mode 100644
index d4bcb8c869c..00000000000
--- a/app/assets/javascripts/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-#import "~/graphql_shared/fragments/page_info.fragment.graphql"
-
-query searchNamespacesWhereUserCanTransferProjects(
- $search: String = ""
- $after: String = ""
- $first: Int = null
-) {
- currentUser {
- id
- groups(permissionScope: TRANSFER_PROJECTS, search: $search, after: $after, first: $first) {
- nodes {
- id
- fullName
- }
- pageInfo {
- ...PageInfo
- }
- }
- namespace {
- id
- fullName
- }
- }
-}
diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
index bc1aff640d2..89c158a9ba8 100644
--- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js
+++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js
@@ -12,6 +12,7 @@ export default () => {
Vue.use(VueApollo);
const {
+ projectId,
targetFormId = null,
targetHiddenInputId = null,
buttonText: confirmButtonText = '',
@@ -26,6 +27,7 @@ export default () => {
}),
provide: {
confirmDangerMessage,
+ projectId,
},
render(createElement) {
return createElement(TransferProjectForm, {
diff --git a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
new file mode 100644
index 00000000000..611561e38f2
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import DefaultBranchSelector from './components/default_branch_selector.vue';
+
+export default (el) => {
+ if (!el) {
+ return null;
+ }
+
+ const { projectId, defaultBranch } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(DefaultBranchSelector, {
+ props: {
+ persistedDefaultBranch: defaultBranch,
+ projectId,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index e8eaf0a70b2..94793a535cc 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import branchRulesQuery from './graphql/queries/branch_rules.query.graphql';
import BranchRule from './components/branch_rule.vue';
@@ -31,14 +31,13 @@ export default {
return data.project?.branchRules?.nodes || [];
},
error() {
- createFlash({ message: this.$options.i18n.queryError });
+ createAlert({ message: this.$options.i18n.queryError });
},
},
},
- props: {
+ inject: {
projectPath: {
- type: String,
- required: true,
+ default: '',
},
},
data() {
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 68750318029..2b88f8561d7 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -1,10 +1,11 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
protectedLabel: s__('BranchRules|protected'),
+ detailsButtonLabel: s__('BranchRules|Details'),
};
export default {
@@ -12,6 +13,12 @@ export default {
i18n,
components: {
GlBadge,
+ GlButton,
+ },
+ inject: {
+ branchRulesPath: {
+ default: '',
+ },
},
props: {
name: {
@@ -38,24 +45,30 @@ export default {
hasApprovalDetails() {
return this.approvalDetails && this.approvalDetails.length;
},
+ detailsPath() {
+ return `${this.branchRulesPath}?branch=${this.name}`;
+ },
},
};
</script>
<template>
- <div class="gl-border-b gl-pt-5 gl-pb-5">
- <strong class="gl-font-monospace">{{ name }}</strong>
+ <div class="gl-border-b gl-pt-5 gl-pb-5 gl-display-flex gl-justify-content-space-between">
+ <div>
+ <strong class="gl-font-monospace">{{ name }}</strong>
- <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
- $options.i18n.defaultLabel
- }}</gl-badge>
+ <gl-badge v-if="isDefault" variant="info" size="sm" class="gl-ml-2">{{
+ $options.i18n.defaultLabel
+ }}</gl-badge>
- <gl-badge v-if="isProtected" variant="success" size="sm" class="gl-ml-2">{{
- $options.i18n.protectedLabel
- }}</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>
+ <ul v-if="hasApprovalDetails" class="gl-pl-6 gl-mt-2 gl-mb-0 gl-text-gray-500">
+ <li v-for="(detail, index) in approvalDetails" :key="index">{{ detail }}</li>
+ </ul>
+ </div>
+ <gl-button :href="detailsPath"> {{ $options.i18n.detailsButtonLabel }}</gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
index 35322e2e466..042be089e09 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js
@@ -12,17 +12,17 @@ const apolloProvider = new VueApollo({
export default function mountBranchRules(el) {
if (!el) return null;
- const { projectPath } = el.dataset;
+ const { projectPath, branchRulesPath } = el.dataset;
return new Vue({
el,
apolloProvider,
+ provide: {
+ projectPath,
+ branchRulesPath,
+ },
render(createElement) {
- return createElement(BranchRulesApp, {
- props: {
- projectPath,
- },
- });
+ return createElement(BranchRulesApp);
},
});
}
diff --git a/app/assets/javascripts/projects/star.js b/app/assets/javascripts/projects/star.js
index e063064663b..55c3d68cd11 100644
--- a/app/assets/javascripts/projects/star.js
+++ b/app/assets/javascripts/projects/star.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
@@ -7,7 +7,7 @@ export default class Star {
constructor(containerSelector = '.project-home-panel') {
const container = document.querySelector(containerSelector);
const starToggle = container.querySelector('.toggle-star');
- starToggle.addEventListener('click', function toggleStarClickCallback() {
+ starToggle?.addEventListener('click', function toggleStarClickCallback() {
const starSpan = starToggle.querySelector('span');
const starIcon = starToggle.querySelector('svg');
const iconClasses = Array.from(starIcon.classList.values());
@@ -34,7 +34,7 @@ export default class Star {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Star toggle failed. Try again later.'),
}),
);
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index 6b14ebadacc..9f9b6424125 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import Poll from '~/lib/utils/poll';
import { __, s__, sprintf } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@@ -57,7 +57,7 @@ export default {
group: 'notfound',
};
this.isLoading = false;
- createFlash({
+ createAlert({
message: __('Something went wrong on our end'),
});
},
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 16eb5c3de32..120f75d4f0c 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import CreateItemDropdown from '~/create_item_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -74,7 +74,7 @@ export default class ProtectedBranchCreate {
$allowedToPush.length
);
- this.$form.find('input[type="submit"]').attr('disabled', toggle);
+ this.$form.find('button[type="submit"]').attr('disabled', toggle);
}
static getProtectedBranches(term, callback) {
@@ -130,7 +130,7 @@ export default class ProtectedBranchCreate {
window.location.reload();
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to protect the branch'),
}),
);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 15e706e38c6..1693d869b54 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,5 +1,5 @@
import { find } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import AccessDropdown from '~/projects/settings/access_dropdown';
@@ -74,7 +74,7 @@ export default class ProtectedBranchEdit {
})
.then(callback)
.catch(() => {
- createFlash({ message: __('Failed to update branch!') });
+ createAlert({ message: __('Failed to update branch!') });
});
}
@@ -141,7 +141,7 @@ export default class ProtectedBranchEdit {
.catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
- createFlash({ message: __('Failed to update branch!') });
+ createAlert({ message: __('Failed to update branch!') });
});
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index 1fe9a753e1e..40c52eba99e 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '../lib/utils/axios_utils';
import { FAILED_TO_UPDATE_TAG_MESSAGE } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
@@ -49,7 +49,7 @@ export default class ProtectedTagEdit {
this.$allowedToCreateDropdownButton.enable();
window.scrollTo({ top: 0, behavior: 'smooth' });
- createFlash({
+ createAlert({
message: FAILED_TO_UPDATE_TAG_MESSAGE,
});
});
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 1343ad8246c..b75958e2ced 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -29,6 +29,7 @@ export default {
GlLoadingIcon,
RefResultsSection,
},
+ inheritAttrs: false,
props: {
enabledRefTypes: {
type: Array,
@@ -70,6 +71,15 @@ export default {
required: false,
default: true,
},
+
+ /* Underlying form field name for scenarios where ref_selector
+ * is used as part of submitting an HTML form
+ */
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -213,89 +223,103 @@ export default {
</script>
<template>
- <gl-dropdown
- :header-text="i18n.dropdownHeader"
- :toggle-class="toggleButtonClass"
- :text="buttonText"
- class="ref-selector"
- v-bind="$attrs"
- v-on="$listeners"
- @shown="focusSearchBox"
- >
- <template #header>
- <gl-search-box-by-type
- ref="searchBox"
- v-model.trim="query"
- :placeholder="i18n.searchPlaceholder"
- autocomplete="off"
- @input="onSearchBoxInput"
- @keydown.enter.prevent="onSearchBoxEnter"
- />
- </template>
+ <div>
+ <gl-dropdown
+ :header-text="i18n.dropdownHeader"
+ :toggle-class="toggleButtonClass"
+ :text="buttonText"
+ class="ref-selector gl-w-full"
+ v-bind="$attrs"
+ v-on="$listeners"
+ @shown="focusSearchBox"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ ref="searchBox"
+ v-model.trim="query"
+ :placeholder="i18n.searchPlaceholder"
+ autocomplete="off"
+ data-qa-selector="ref_selector_searchbox"
+ @input="onSearchBoxInput"
+ @keydown.enter.prevent="onSearchBoxEnter"
+ />
+ </template>
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
- <div v-else-if="showNoResults" class="gl-text-center gl-mx-3 gl-py-3" data-testid="no-results">
- <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
- <template #query>
- <b class="gl-word-break-all">{{ lastQuery }}</b>
- </template>
- </gl-sprintf>
+ <div
+ v-else-if="showNoResults"
+ class="gl-text-center gl-mx-3 gl-py-3"
+ data-testid="no-results"
+ >
+ <gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
+ <template #query>
+ <b class="gl-word-break-all">{{ lastQuery }}</b>
+ </template>
+ </gl-sprintf>
- <span v-else>{{ i18n.noResults }}</span>
- </div>
+ <span v-else>{{ i18n.noResults }}</span>
+ </div>
- <template v-else>
- <template v-if="showBranchesSection">
- <ref-results-section
- :section-title="i18n.branches"
- :total-count="matches.branches.totalCount"
- :items="matches.branches.list"
- :selected-ref="selectedRef"
- :error="matches.branches.error"
- :error-message="i18n.branchesErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="branches-section"
- data-qa-selector="branches_section"
- @selected="selectRef($event)"
- />
+ <template v-else>
+ <template v-if="showBranchesSection">
+ <ref-results-section
+ :section-title="i18n.branches"
+ :total-count="matches.branches.totalCount"
+ :items="matches.branches.list"
+ :selected-ref="selectedRef"
+ :error="matches.branches.error"
+ :error-message="i18n.branchesErrorMessage"
+ :show-header="showSectionHeaders"
+ data-testid="branches-section"
+ data-qa-selector="branches_section"
+ @selected="selectRef($event)"
+ />
- <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
- </template>
+ <gl-dropdown-divider v-if="showTagsSection || showCommitsSection" />
+ </template>
- <template v-if="showTagsSection">
- <ref-results-section
- :section-title="i18n.tags"
- :total-count="matches.tags.totalCount"
- :items="matches.tags.list"
- :selected-ref="selectedRef"
- :error="matches.tags.error"
- :error-message="i18n.tagsErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="tags-section"
- @selected="selectRef($event)"
- />
+ <template v-if="showTagsSection">
+ <ref-results-section
+ :section-title="i18n.tags"
+ :total-count="matches.tags.totalCount"
+ :items="matches.tags.list"
+ :selected-ref="selectedRef"
+ :error="matches.tags.error"
+ :error-message="i18n.tagsErrorMessage"
+ :show-header="showSectionHeaders"
+ data-testid="tags-section"
+ @selected="selectRef($event)"
+ />
- <gl-dropdown-divider v-if="showCommitsSection" />
- </template>
+ <gl-dropdown-divider v-if="showCommitsSection" />
+ </template>
- <template v-if="showCommitsSection">
- <ref-results-section
- :section-title="i18n.commits"
- :total-count="matches.commits.totalCount"
- :items="matches.commits.list"
- :selected-ref="selectedRef"
- :error="matches.commits.error"
- :error-message="i18n.commitsErrorMessage"
- :show-header="showSectionHeaders"
- data-testid="commits-section"
- @selected="selectRef($event)"
- />
+ <template v-if="showCommitsSection">
+ <ref-results-section
+ :section-title="i18n.commits"
+ :total-count="matches.commits.totalCount"
+ :items="matches.commits.list"
+ :selected-ref="selectedRef"
+ :error="matches.commits.error"
+ :error-message="i18n.commitsErrorMessage"
+ :show-header="showSectionHeaders"
+ data-testid="commits-section"
+ @selected="selectRef($event)"
+ />
+ </template>
</template>
- </template>
- <template #footer>
- <slot name="footer" v-bind="footerSlotProps"></slot>
- </template>
- </gl-dropdown>
+ <template #footer>
+ <slot name="footer" v-bind="footerSlotProps"></slot>
+ </template>
+ </gl-dropdown>
+ <input
+ v-if="name"
+ data-testid="selected-ref-form-field"
+ type="hidden"
+ :value="selectedRef"
+ :name="name"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue
index 53f2dbbbbd7..1ab41ee2f0a 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_block.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue
@@ -233,6 +233,7 @@ export default {
<div
v-if="isFormVisible"
class="js-add-related-issues-form-area card-body bordered-box bg-white"
+ :class="{ 'gl-mb-5': shouldShowTokenBody }"
>
<add-issuable-form
:show-categorized-issues="showCategorizedIssues"
@@ -253,7 +254,7 @@ export default {
</div>
<template v-if="shouldShowTokenBody">
<related-issues-list
- v-for="category in categorisedIssues"
+ v-for="(category, index) in categorisedIssues"
:key="category.linkType"
:list-link-type="category.linkType"
:heading="$options.linkedIssueTypesTextMap[category.linkType]"
@@ -263,6 +264,7 @@ export default {
:issuable-type="issuableType"
:path-id-separator="pathIdSeparator"
:related-issues="category.issues"
+ :class="{ 'gl-mt-5': index > 0 }"
@relatedIssueRemoveRequest="$emit('relatedIssueRemoveRequest', $event)"
@saveReorder="$emit('saveReorder', $event)"
/>
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue
index ae40232df6f..38e1d6e9d4f 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_root.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue
@@ -23,7 +23,7 @@ Your caret can stop touching a `rawReference` can happen in a variety of ways:
and hide the `AddIssuableForm` area.
*/
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import {
@@ -141,11 +141,11 @@ export default {
})
.catch((res) => {
if (res && res.status !== 404) {
- createFlash({ message: relatedIssuesRemoveErrorMap[this.issuableType] });
+ createAlert({ message: relatedIssuesRemoveErrorMap[this.issuableType] });
}
});
} else {
- createFlash({ message: pathIndeterminateErrorMap[this.issuableType] });
+ createAlert({ message: pathIndeterminateErrorMap[this.issuableType] });
}
},
onToggleAddRelatedIssuesForm() {
@@ -174,7 +174,7 @@ export default {
if (response && response.data && response.data.message) {
errorMessage = response.data.message;
}
- createFlash({ message: errorMessage });
+ createAlert({ message: errorMessage });
})
.finally(() => {
this.isSubmitting = false;
@@ -195,7 +195,7 @@ export default {
})
.catch(() => {
this.store.setRelatedIssues([]);
- createFlash({ message: __('An error occurred while fetching issues.') });
+ createAlert({ message: __('An error occurred while fetching issues.') });
})
.finally(() => {
this.isFetching = false;
@@ -216,7 +216,7 @@ export default {
}
})
.catch(() => {
- createFlash({ message: __('An error occurred while reordering issues.') });
+ createAlert({ message: __('An error occurred while reordering issues.') });
});
}
},
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index d63a83d1a08..6dc8240e680 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { historyPushState } from '~/lib/utils/common_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
@@ -71,7 +71,7 @@ export default {
error(error) {
this.fullRequestError = true;
- createFlash({
+ createAlert({
message: this.$options.i18n.errorMessage,
captureError: true,
error,
diff --git a/app/assets/javascripts/releases/components/app_show.vue b/app/assets/javascripts/releases/components/app_show.vue
index fdb0f99b735..7147cfa01c8 100644
--- a/app/assets/javascripts/releases/components/app_show.vue
+++ b/app/assets/javascripts/releases/components/app_show.vue
@@ -1,5 +1,5 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import oneReleaseQuery from '../graphql/queries/one_release.query.graphql';
import { convertGraphQLRelease } from '../util';
@@ -51,7 +51,7 @@ export default {
},
methods: {
showFlash(error) {
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while getting the release details.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 08b727dcca0..2ddab5dddea 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -1,8 +1,15 @@
<script>
-import { GlFormGroup, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import {
+ GlCollapse,
+ GlLink,
+ GlFormGroup,
+ GlFormTextarea,
+ GlDropdownItem,
+ GlSprintf,
+} from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapState, mapActions, mapGetters } from 'vuex';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_TAGS } from '~/ref/constants';
import FormFieldContainer from './form_field_container.vue';
@@ -10,7 +17,10 @@ import FormFieldContainer from './form_field_container.vue';
export default {
name: 'TagFieldNew',
components: {
+ GlCollapse,
GlFormGroup,
+ GlFormTextarea,
+ GlLink,
RefSelector,
FormFieldContainer,
GlDropdownItem,
@@ -41,6 +51,14 @@ export default {
this.updateShowCreateFrom(false);
},
},
+ tagMessage: {
+ get() {
+ return this.release.tagMessage;
+ },
+ set(tagMessage) {
+ this.updateReleaseTagMessage(tagMessage);
+ },
+ },
createFromModel: {
get() {
return this.createFrom;
@@ -70,6 +88,7 @@ export default {
methods: {
...mapActions('editNew', [
'updateReleaseTagName',
+ 'updateReleaseTagMessage',
'updateCreateFrom',
'fetchTagNotes',
'updateShowCreateFrom',
@@ -113,9 +132,20 @@ export default {
noRefSelected: __('No source selected'),
searchPlaceholder: __('Search branches, tags, and commits'),
dropdownHeader: __('Select source'),
+ label: __('Create from'),
+ description: __('Existing branch name, tag, or commit SHA'),
+ },
+ annotatedTag: {
+ label: s__('CreateGitTag|Set tag message'),
+ description: s__(
+ 'CreateGitTag|Add a message to the tag. Leaving this blank creates a %{linkStart}lightweight tag%{linkEnd}.',
+ ),
},
},
+ tagMessageId: uniqueId('tag-message-'),
+
tagNameEnabledRefTypes: [REF_TYPE_TAGS],
+ gitTagDocsLink: 'https://git-scm.com/book/en/v2/Git-Basics-Tagging/',
};
</script>
<template>
@@ -156,23 +186,45 @@ export default {
</ref-selector>
</form-field-container>
</gl-form-group>
- <gl-form-group
- v-if="showCreateFrom"
- :label="__('Create from')"
- :label-for="createFromSelectorId"
- data-testid="create-from-field"
- >
- <form-field-container>
- <ref-selector
- :id="createFromSelectorId"
- v-model="createFromModel"
- :project-id="projectId"
- :translations="$options.translations.createFrom"
- />
- </form-field-container>
- <template #description>
- {{ __('Existing branch name, tag, or commit SHA') }}
- </template>
- </gl-form-group>
+ <gl-collapse :visible="showCreateFrom">
+ <div class="gl-pl-6 gl-border-l-1 gl-border-l-solid gl-border-gray-300">
+ <gl-form-group
+ v-if="showCreateFrom"
+ :label="$options.translations.createFrom.label"
+ :label-for="createFromSelectorId"
+ data-testid="create-from-field"
+ >
+ <form-field-container>
+ <ref-selector
+ :id="createFromSelectorId"
+ v-model="createFromModel"
+ :project-id="projectId"
+ :translations="$options.translations.createFrom"
+ />
+ </form-field-container>
+ <template #description>{{ $options.translations.createFrom.description }}</template>
+ </gl-form-group>
+ <gl-form-group
+ v-if="showCreateFrom"
+ :label="$options.translations.annotatedTag.label"
+ :label-for="$options.tagMessageId"
+ data-testid="annotated-tag-message-field"
+ >
+ <gl-form-textarea :id="$options.tagMessageId" v-model="tagMessage" />
+ <template #description>
+ <gl-sprintf :message="$options.translations.annotatedTag.description">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.gitTagDocsLink"
+ rel="noopener noreferrer"
+ target="_blank"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-form-group>
+ </div>
+ </gl-collapse>
</div>
</template>
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 669e5928143..42ceed81c00 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -1,5 +1,5 @@
import { getTag } from '~/rest_api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
@@ -48,7 +48,7 @@ export const fetchRelease = async ({ commit, state }) => {
commit(types.RECEIVE_RELEASE_SUCCESS, release);
} catch (error) {
commit(types.RECEIVE_RELEASE_ERROR, error);
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while getting the release details.'),
});
}
@@ -57,6 +57,9 @@ export const fetchRelease = async ({ commit, state }) => {
export const updateReleaseTagName = ({ commit }, tagName) =>
commit(types.UPDATE_RELEASE_TAG_NAME, tagName);
+export const updateReleaseTagMessage = ({ commit }, tagMessage) =>
+ commit(types.UPDATE_RELEASE_TAG_MESSAGE, tagMessage);
+
export const updateCreateFrom = ({ commit }, createFrom) =>
commit(types.UPDATE_CREATE_FROM, createFrom);
@@ -133,11 +136,11 @@ export const createRelease = async ({ commit, dispatch, getters }) => {
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
if (error instanceof GraphQLError) {
- createFlash({
+ createAlert({
message: error.message,
});
} else {
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while creating a new release.'),
});
}
@@ -219,7 +222,7 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
dispatch('receiveSaveReleaseSuccess', state.release._links.self);
} catch (error) {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while saving the release details.'),
});
}
@@ -233,7 +236,7 @@ export const fetchTagNotes = ({ commit, state }, tagName) => {
commit(types.RECEIVE_TAG_NOTES_SUCCESS, data);
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('Release|Unable to fetch the tag notes.'),
});
@@ -266,7 +269,7 @@ export const deleteRelease = ({ commit, getters, dispatch, state }) => {
})
.catch((error) => {
commit(types.RECEIVE_SAVE_RELEASE_ERROR, error);
- createFlash({
+ createAlert({
message: s__('Release|Something went wrong while deleting the release.'),
});
});
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index ccca9ca8250..0d77095d099 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -145,6 +145,7 @@ export const releaseCreateMutatationVariables = (state, getters) => {
input: {
...getters.releaseUpdateMutatationVariables.input,
ref: state.createFrom,
+ tagMessage: state.release.tagMessage,
assets: {
links: getters.releaseLinksToCreate.map(({ name, url, linkType }) => ({
name: name.trim(),
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
index 0ef017f4eb4..e52eccd6a21 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutation_types.js
@@ -5,6 +5,7 @@ export const RECEIVE_RELEASE_SUCCESS = 'RECEIVE_RELEASE_SUCCESS';
export const RECEIVE_RELEASE_ERROR = 'RECEIVE_RELEASE_ERROR';
export const UPDATE_RELEASE_TAG_NAME = 'UPDATE_RELEASE_TAG_NAME';
+export const UPDATE_RELEASE_TAG_MESSAGE = 'UPDATE_RELEASE_TAG_MESSAGE';
export const UPDATE_CREATE_FROM = 'UPDATE_CREATE_FROM';
export const UPDATE_SHOW_CREATE_FROM = 'UPDATE_SHOW_CREATE_FROM';
export const UPDATE_RELEASE_TITLE = 'UPDATE_RELEASE_TITLE';
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
index 34361f84a5a..f80e75501c9 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/mutations.js
@@ -10,6 +10,7 @@ export default {
[types.INITIALIZE_EMPTY_RELEASE](state) {
state.release = {
tagName: state.tagName,
+ tagMessage: '',
name: '',
description: '',
milestones: [],
@@ -40,6 +41,9 @@ export default {
[types.UPDATE_RELEASE_TAG_NAME](state, tagName) {
state.release.tagName = tagName;
},
+ [types.UPDATE_RELEASE_TAG_MESSAGE](state, tagMessage) {
+ state.release.tagMessage = tagMessage;
+ },
[types.UPDATE_CREATE_FROM](state, createFrom) {
state.createFrom = createFrom;
},
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
index 11a2f9df59b..3112becfa9e 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
@@ -37,7 +37,7 @@ export default ({
* When creating a new release, this is the default from the URL
*/
tagName,
- showCreateFrom: !tagName,
+ showCreateFrom: false,
defaultBranch,
createFrom: defaultBranch,
diff --git a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue b/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
deleted file mode 100644
index 05ab5c2cc90..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/components/accessibility_issue_body.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlBadge, GlLink } from '@gitlab/ui';
-
-export default {
- name: 'AccessibilityIssueBody',
- components: {
- GlBadge,
- GlLink,
- },
- props: {
- issue: {
- type: Object,
- required: true,
- },
- isNew: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- parsedTECHSCode() {
- /*
- * In issue code looks like "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail"
- * or "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent"
- *
- * The TECHS code is the "G18", "G168", "H91", etc. from the code which is used for the documentation.
- * Here we simply split the string on `.` and get the code in the 5th position
- */
- return this.issue.code?.split('.')[4];
- },
- learnMoreUrl() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `https://www.w3.org/TR/WCAG20-TECHS/${this.parsedTECHSCode || 'Overview'}.html`;
- },
- },
-};
-</script>
-<template>
- <div class="report-block-list-issue-description gl-mt-2 gl-mb-2">
- <div ref="accessibility-issue-description" class="report-block-list-issue-description-text">
- <gl-badge v-if="isNew" class="gl-mr-2" variant="danger">{{
- s__('AccessibilityReport|New')
- }}</gl-badge>
- <div>
- {{
- sprintf(
- s__(
- 'AccessibilityReport|The accessibility scanning found an error of the following type: %{code}',
- ),
- { code: issue.code },
- )
- }}
- <gl-link ref="accessibility-issue-learn-more" :href="learnMoreUrl" target="_blank">{{
- s__('AccessibilityReport|Learn more')
- }}</gl-link>
- </div>
- {{ sprintf(s__('AccessibilityReport|Message: %{message}'), { message: issue.message }) }}
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue b/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
deleted file mode 100644
index 99cdeae545e..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/grouped_accessibility_reports_app.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import { mapActions, mapGetters } from 'vuex';
-import { componentNames } from '~/reports/components/issue_body';
-import IssuesList from '~/reports/components/issues_list.vue';
-import ReportSection from '~/reports/components/report_section.vue';
-import createStore from './store';
-
-export default {
- name: 'GroupedAccessibilityReportsApp',
- store: createStore(),
- components: {
- ReportSection,
- IssuesList,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- componentNames,
- computed: {
- ...mapGetters([
- 'summaryStatus',
- 'groupedSummaryText',
- 'shouldRenderIssuesList',
- 'unresolvedIssues',
- 'resolvedIssues',
- 'newIssues',
- ]),
- },
- created() {
- this.setEndpoint(this.endpoint);
-
- this.fetchReport();
- },
- methods: {
- ...mapActions(['fetchReport', 'setEndpoint']),
- },
-};
-</script>
-<template>
- <report-section
- :status="summaryStatus"
- :success-text="groupedSummaryText"
- :loading-text="groupedSummaryText"
- :error-text="groupedSummaryText"
- :has-issues="shouldRenderIssuesList"
- track-action="users_expanding_testing_accessibility_report"
- class="mr-widget-section grouped-security-reports mr-report"
- >
- <template #body>
- <div class="mr-widget-grouped-section report-block">
- <issues-list
- v-if="shouldRenderIssuesList"
- :unresolved-issues="unresolvedIssues"
- :new-issues="newIssues"
- :resolved-issues="resolvedIssues"
- :component="$options.componentNames.AccessibilityIssueBody"
- class="report-block-group-list"
- />
- </div>
- </template>
- </report-section>
-</template>
diff --git a/app/assets/javascripts/reports/accessibility_report/store/actions.js b/app/assets/javascripts/reports/accessibility_report/store/actions.js
deleted file mode 100644
index e0142a35291..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/actions.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import Visibility from 'visibilityjs';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import Poll from '~/lib/utils/poll';
-import * as types from './mutation_types';
-
-let eTagPoll;
-
-export const clearEtagPoll = () => {
- eTagPoll = null;
-};
-
-export const stopPolling = () => {
- if (eTagPoll) eTagPoll.stop();
-};
-
-export const restartPolling = () => {
- if (eTagPoll) eTagPoll.restart();
-};
-
-export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
-
-/**
- * We need to poll the report endpoint while they are being parsed in the Backend.
- * This can take up to one minute.
- *
- * Poll.js will handle etag response.
- * While http status code is 204, it means it's parsing, and we'll keep polling
- * When http status code is 200, it means parsing is done, we can show the results & stop polling
- * When http status code is 500, it means parsing went wrong and we stop polling
- */
-export const fetchReport = ({ state, dispatch, commit }) => {
- commit(types.REQUEST_REPORT);
-
- eTagPoll = new Poll({
- resource: {
- getReport(endpoint) {
- return axios.get(endpoint);
- },
- },
- data: state.endpoint,
- method: 'getReport',
- successCallback: ({ status, data }) => dispatch('receiveReportSuccess', { status, data }),
- errorCallback: () => dispatch('receiveReportError'),
- });
-
- if (!Visibility.hidden()) {
- eTagPoll.makeRequest();
- } else {
- axios
- .get(state.endpoint)
- .then(({ status, data }) => dispatch('receiveReportSuccess', { status, data }))
- .catch(() => dispatch('receiveReportError'));
- }
-
- Visibility.change(() => {
- if (!Visibility.hidden() && state.isLoading) {
- dispatch('restartPolling');
- } else {
- dispatch('stopPolling');
- }
- });
-};
-
-export const receiveReportSuccess = ({ commit, dispatch }, { status, data }) => {
- if (status === httpStatusCodes.OK) {
- commit(types.RECEIVE_REPORT_SUCCESS, data);
- // Stop polling since we have the information already parsed and it won't be changing
- dispatch('stopPolling');
- }
-};
-
-export const receiveReportError = ({ commit, dispatch }) => {
- commit(types.RECEIVE_REPORT_ERROR);
- dispatch('stopPolling');
-};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/getters.js b/app/assets/javascripts/reports/accessibility_report/store/getters.js
deleted file mode 100644
index 20506b1bfd1..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/getters.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { s__, n__ } from '~/locale';
-import { LOADING, ERROR, SUCCESS, STATUS_FAILED } from '../../constants';
-
-export const groupedSummaryText = (state) => {
- if (state.isLoading) {
- return s__('Reports|Accessibility scanning results are being parsed');
- }
-
- if (state.hasError) {
- return s__('Reports|Accessibility scanning failed loading results');
- }
-
- const numberOfResults = state.report?.summary?.errored || 0;
- if (numberOfResults === 0) {
- return s__('Reports|Accessibility scanning detected no issues for the source branch only');
- }
-
- return n__(
- 'Reports|Accessibility scanning detected %d issue for the source branch only',
- 'Reports|Accessibility scanning detected %d issues for the source branch only',
- numberOfResults,
- );
-};
-
-export const summaryStatus = (state) => {
- if (state.isLoading) {
- return LOADING;
- }
-
- if (state.hasError || state.status === STATUS_FAILED) {
- return ERROR;
- }
-
- return SUCCESS;
-};
-
-export const shouldRenderIssuesList = (state) =>
- Object.values(state.report).some((x) => Array.isArray(x) && x.length > 0);
-
-// We could just map state, but we're going to iterate in the future
-// to add notes and warnings to these issue lists, so I'm going to
-// keep these as getters
-export const unresolvedIssues = (state) => state.report.existing_errors;
-export const resolvedIssues = (state) => state.report.resolved_errors;
-export const newIssues = (state) => state.report.new_errors;
diff --git a/app/assets/javascripts/reports/accessibility_report/store/index.js b/app/assets/javascripts/reports/accessibility_report/store/index.js
deleted file mode 100644
index 5bfcd69edec..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/index.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const getStoreConfig = (initialState) => ({
- actions,
- getters,
- mutations,
- state: state(initialState),
-});
-
-export default (initialState) => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js b/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
deleted file mode 100644
index 22e2330e1ea..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/mutation_types.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const SET_ENDPOINT = 'SET_ENDPOINT';
-
-export const REQUEST_REPORT = 'REQUEST_REPORT';
-export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
-export const RECEIVE_REPORT_ERROR = 'RECEIVE_REPORT_ERROR';
diff --git a/app/assets/javascripts/reports/accessibility_report/store/mutations.js b/app/assets/javascripts/reports/accessibility_report/store/mutations.js
deleted file mode 100644
index 20d3e5be9a3..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/mutations.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_ENDPOINT](state, endpoint) {
- state.endpoint = endpoint;
- },
- [types.REQUEST_REPORT](state) {
- state.isLoading = true;
- },
- [types.RECEIVE_REPORT_SUCCESS](state, report) {
- state.hasError = false;
- state.isLoading = false;
- state.report = report;
- },
- [types.RECEIVE_REPORT_ERROR](state) {
- state.isLoading = false;
- state.hasError = true;
- state.report = {};
- },
-};
diff --git a/app/assets/javascripts/reports/accessibility_report/store/state.js b/app/assets/javascripts/reports/accessibility_report/store/state.js
deleted file mode 100644
index 2a4cefea5e6..00000000000
--- a/app/assets/javascripts/reports/accessibility_report/store/state.js
+++ /dev/null
@@ -1,28 +0,0 @@
-export default (initialState = {}) => ({
- endpoint: initialState.endpoint || '',
-
- isLoading: initialState.isLoading || false,
- hasError: initialState.hasError || false,
-
- /**
- * Report will have the following format:
- * {
- * status: {String},
- * summary: {
- * total: {Number},
- * resolved: {Number},
- * errored: {Number},
- * },
- * existing_errors: {Array.<Object>},
- * existing_notes: {Array.<Object>},
- * existing_warnings: {Array.<Object>},
- * new_errors: {Array.<Object>},
- * new_notes: {Array.<Object>},
- * new_warnings: {Array.<Object>},
- * resolved_errors: {Array.<Object>},
- * resolved_notes: {Array.<Object>},
- * resolved_warnings: {Array.<Object>},
- * }
- */
- report: initialState.report || {},
-});
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 04e72809e62..a76a6f45c07 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,14 +1,11 @@
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
export const components = {
- AccessibilityIssueBody: () =>
- import('../accessibility_report/components/accessibility_issue_body.vue'),
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
TestIssueBody: () => import('../grouped_test_report/components/test_issue_body.vue'),
};
export const componentNames = {
- AccessibilityIssueBody: 'AccessibilityIssueBody',
CodequalityIssueBody: 'CodequalityIssueBody',
TestIssueBody: 'TestIssueBody',
};
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 6061be465ca..bb86695b9a3 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -1,7 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
import api from '~/api';
-import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -115,9 +114,6 @@ export default {
},
computed: {
- collapseText() {
- return this.isCollapsed ? __('Expand') : __('Collapse');
- },
isLoading() {
return this.status === status.LOADING;
},
@@ -172,6 +168,11 @@ export default {
},
methods: {
toggleCollapsed() {
+ // Because the top-level div is always clickable, we need to check if we can collapse.
+ if (!this.isCollapsible) {
+ return;
+ }
+
if (this.trackAction) {
api.trackRedisHllUserEvent(this.trackAction);
}
@@ -186,10 +187,13 @@ export default {
</script>
<template>
<section class="media-section">
- <div class="media">
+ <div class="media" :class="{ 'gl-cursor-pointer': isCollapsible }" @click="toggleCollapsed">
<status-icon :status="statusIconName" :size="24" class="align-self-center" />
- <div class="media-body d-flex flex-align-self-center align-items-center">
- <div data-testid="report-section-code-text" class="js-code-text code-text">
+ <div class="media-body gl-display-flex gl-align-items-flex-start gl-flex-direction-row!">
+ <div
+ data-testid="report-section-code-text"
+ class="js-code-text code-text gl-align-self-center gl-flex-grow-1"
+ >
<div class="gl-display-flex gl-align-items-center">
<p class="gl-line-height-normal gl-m-0">{{ headerText }}</p>
<slot :name="slotName"></slot>
@@ -204,14 +208,19 @@ export default {
<slot name="action-buttons" :is-collapsible="isCollapsible"></slot>
- <gl-button
+ <div
v-if="isCollapsible"
- data-testid="report-section-expand-button"
- data-qa-selector="expand_report_button"
- @click="toggleCollapsed"
+ class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3"
>
- {{ collapseText }}
- </gl-button>
+ <gl-button
+ data-testid="report-section-expand-button"
+ data-qa-selector="expand_report_button"
+ category="tertiary"
+ size="small"
+ :icon="isExpanded ? 'chevron-lg-up' : 'chevron-lg-down'"
+ @click.stop="toggleCollapsed"
+ />
+ </div>
</div>
</div>
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index 5fd9cfd4e53..f009c0310c5 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -1,7 +1,7 @@
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { normalizeData } from 'ee_else_ce/repository/utils/commit';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { COMMIT_BATCH_SIZE, I18N_COMMIT_DATA_FETCH_ERROR } from './constants';
let requestedOffsets = [];
@@ -43,7 +43,7 @@ const fetchData = (projectPath, path, ref, offset) => {
return axios
.get(url, { params: { format: 'json', offset } })
.then(({ data }) => normalizeData(data, path))
- .catch(() => createFlash({ message: I18N_COMMIT_DATA_FETCH_ERROR }));
+ .catch(() => createAlert({ message: I18N_COMMIT_DATA_FETCH_ERROR }));
};
export const loadCommits = async (projectPath, path, ref, offset) => {
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 902077ba3e4..bf1667d8734 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -4,7 +4,7 @@ import { uniqueId } from 'lodash';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -271,7 +271,7 @@ export default {
.catch(() => this.displayError());
},
displayError() {
- createFlash({ message: __('An error occurred while loading the file. Please try again.') });
+ createAlert({ message: __('An error occurred while loading the file. Please try again.') });
},
switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue
index fb1227f0df9..29c2c3762fc 100644
--- a/app/assets/javascripts/repository/components/blob_controls.vue
+++ b/app/assets/javascripts/repository/components/blob_controls.vue
@@ -1,9 +1,11 @@
<script>
import { GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import getRefMixin from '~/repository/mixins/get_ref';
import initSourcegraph from '~/sourcegraph';
+import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
+import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import { updateElementsVisibility } from '../utils/dom';
import blobControlsQuery from '../queries/blob_controls.query.graphql';
@@ -34,7 +36,7 @@ export default {
return !this.filePath;
},
error() {
- createFlash({ message: this.$options.i18n.errorMessage });
+ createAlert({ message: this.$options.i18n.errorMessage });
},
},
},
@@ -84,6 +86,33 @@ export default {
},
blobInfo() {
initSourcegraph();
+ this.$nextTick(() => {
+ this.initShortcuts();
+ this.initLinksUpdate();
+ });
+ },
+ },
+ methods: {
+ initShortcuts() {
+ const fileBlobPermalinkUrlElement = document.querySelector(
+ '.js-data-file-blob-permalink-url',
+ );
+ const fileBlobPermalinkUrl =
+ fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
+ // eslint-disable-next-line no-new
+ new ShortcutsBlob({
+ skipResetBindings: true,
+ fileBlobPermalinkUrl,
+ fileBlobPermalinkUrlElement,
+ });
+ },
+ initLinksUpdate() {
+ // eslint-disable-next-line no-new
+ new BlobLinePermalinkUpdater(
+ document.querySelector('.tree-holder'),
+ '.file-line-num[data-line-number], .file-line-num[data-line-number] *',
+ document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'),
+ );
},
},
};
@@ -99,6 +128,7 @@ export default {
data-testid="blame"
:href="blobInfo.blamePath"
:class="$options.buttonClassList"
+ class="js-blob-blame-link"
>
{{ $options.i18n.blame }}
</gl-button>
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 22fe3fe440e..05d64077866 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -122,14 +122,12 @@ export default {
:link-href="commit.author.webPath"
:img-src="commit.author.avatarUrl"
:img-size="32"
- :img-css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
class="gl-my-2 gl-mr-4"
/>
<user-avatar-image
v-else
class="gl-my-2 gl-mr-4"
:img-src="commit.authorGravatar || $options.defaultAvatarUrl"
- :css-classes="'gl-mr-0!' /* NOTE: this is needed only while we migrate user-avatar-image to GlAvatar (7731 epics) */"
:size="32"
/>
<div
@@ -171,7 +169,7 @@ export default {
v-if="commitDescription"
v-safe-html:[$options.safeHtmlConfig]="commitDescription"
:class="{ 'd-block': showDescription }"
- class="commit-row-description gl-mb-3"
+ class="commit-row-description gl-mb-3 gl-white-space-pre-line"
></pre>
</div>
<div class="gl-flex-grow-1"></div>
diff --git a/app/assets/javascripts/repository/components/new_directory_modal.vue b/app/assets/javascripts/repository/components/new_directory_modal.vue
index 6c5797bf5b2..b28ebe7bb1e 100644
--- a/app/assets/javascripts/repository/components/new_directory_modal.vue
+++ b/app/assets/javascripts/repository/components/new_directory_modal.vue
@@ -8,7 +8,7 @@ import {
GlFormTextarea,
GlToggle,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -140,7 +140,7 @@ export default {
})
.catch(() => {
this.loading = false;
- createFlash({ message: ERROR_MESSAGE });
+ createAlert({ message: ERROR_MESSAGE });
});
},
},
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index c8cd64b5311..f3c5ace75fc 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -260,19 +260,19 @@ export default {
class="ml-1"
/>
</td>
- <td class="d-none d-sm-table-cell tree-commit cursor-default">
+ <td class="d-none d-sm-table-cell tree-commit cursor-default gl-text-secondary">
<gl-link
v-if="commitData"
v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml"
:href="commitData.commitPath"
:title="commitData.message"
- class="str-truncated-100 tree-commit-link"
+ class="str-truncated-100 tree-commit-link gl-text-secondary"
/>
<gl-intersection-observer @appear="rowAppeared" @disappear="rowDisappeared">
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</gl-intersection-observer>
</td>
- <td class="tree-time-ago text-right cursor-default">
+ <td class="tree-time-ago text-right cursor-default gl-text-secondary">
<timeago-tooltip v-if="commitData" :time="commitData.committedDate" />
<gl-skeleton-loader v-if="showSkeletonLoader" :lines="1" />
</td>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 2200e999c75..8a45a351c35 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,6 +1,6 @@
<script>
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '~/locale';
import {
@@ -142,7 +142,7 @@ export default {
}
})
.catch((error) => {
- createFlash({
+ createAlert({
message: __('An error occurred while fetching folder content.'),
});
throw error;
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 7fcaf772aac..4603ea2710d 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -9,7 +9,7 @@ import {
GlButton,
GlAlert,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
@@ -171,7 +171,7 @@ export default {
})
.catch(() => {
this.loading = false;
- createFlash({ message: ERROR_MESSAGE });
+ createAlert({ message: ERROR_MESSAGE });
});
},
formData() {
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index f5620876783..dbaabb35cde 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -17,8 +17,6 @@ import allRunnersCountQuery from 'ee_else_ce/runner/graphql/list/all_runners_cou
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
-import RunnerBulkDelete from '../components/runner_bulk_delete.vue';
-import RunnerBulkDeleteCheckbox from '../components/runner_bulk_delete_checkbox.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerListEmptyState from '../components/runner_list_empty_state.vue';
import RunnerName from '../components/runner_name.vue';
@@ -30,7 +28,12 @@ import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
-import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
+import {
+ ADMIN_FILTERED_SEARCH_NAMESPACE,
+ INSTANCE_TYPE,
+ I18N_FETCH_ERROR,
+ FILTER_CSS_CLASSES,
+} from '../constants';
import { captureException } from '../sentry_utils';
export default {
@@ -40,8 +43,6 @@ export default {
RegistrationDropdown,
RunnerStackedLayoutBanner,
RunnerFilteredSearchBar,
- RunnerBulkDelete,
- RunnerBulkDeleteCheckbox,
RunnerList,
RunnerListEmptyState,
RunnerName,
@@ -51,7 +52,7 @@ export default {
RunnerActionsCell,
},
mixins: [glFeatureFlagMixin()],
- inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath', 'localMutations'],
+ inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
props: {
registrationToken: {
type: String,
@@ -114,11 +115,6 @@ export default {
upgradeStatusTokenConfig,
];
},
- isBulkDeleteEnabled() {
- // Feature flag: admin_runners_bulk_delete
- // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/353981
- return this.glFeatures.adminRunnersBulkDelete;
- },
isSearchFiltered() {
return isSearchFiltered(this.search);
},
@@ -155,18 +151,13 @@ export default {
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
- onChecked({ runner, isChecked }) {
- this.localMutations.setRunnerChecked({
- runner,
- isChecked,
- });
- },
onPaginationInput(value) {
this.search.pagination = value;
},
},
filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
+ FILTER_CSS_CLASSES,
};
</script>
<template>
@@ -195,6 +186,7 @@ export default {
<runner-filtered-search-bar
v-model="search"
+ :class="$options.FILTER_CSS_CLASSES"
:tokens="searchTokens"
:namespace="$options.filteredSearchNamespace"
/>
@@ -209,20 +201,12 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-bulk-delete
- v-if="isBulkDeleteEnabled"
- :runners="runners.items"
- @deleted="onDeleted"
- />
<runner-list
:runners="runners.items"
:loading="runnersLoading"
- :checkable="isBulkDeleteEnabled"
- @checked="onChecked"
+ :checkable="true"
+ @deleted="onDeleted"
>
- <template v-if="isBulkDeleteEnabled" #head-checkbox>
- <runner-bulk-delete-checkbox :runners="runners.items" />
- </template>
<template #runner-name="{ runner }">
<gl-link :href="runner.adminUrl">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index 7a4760f81ee..13f520c4edb 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -52,11 +52,6 @@ export default {
:compact="true"
@toggledPaused="onToggledPaused"
/>
- <runner-delete-button
- :disabled="!canDelete"
- :runner="runner"
- :compact="true"
- @deleted="onDeleted"
- />
+ <runner-delete-button v-if="canDelete" :runner="runner" :compact="true" @deleted="onDeleted" />
</gl-button-group>
</template>
diff --git a/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue b/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue
new file mode 100644
index 00000000000..cb43760b2d6
--- /dev/null
+++ b/app/assets/javascripts/runner/components/cells/runner_owner_cell.vue
@@ -0,0 +1,63 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, I18N_ADMIN } from '../../constants';
+
+export default {
+ components: {
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ cell() {
+ switch (this.runner?.runnerType) {
+ case INSTANCE_TYPE:
+ return {
+ text: I18N_ADMIN,
+ };
+ case GROUP_TYPE: {
+ const { name, fullName, webUrl } = this.runner?.groups?.nodes[0] || {};
+
+ return {
+ text: name,
+ href: webUrl,
+ tooltip: fullName !== name ? fullName : '',
+ };
+ }
+ case PROJECT_TYPE: {
+ const { name, nameWithNamespace, webUrl } = this.runner?.ownerProject || {};
+
+ return {
+ text: name,
+ href: webUrl,
+ tooltip: nameWithNamespace !== name ? nameWithNamespace : '',
+ };
+ }
+ default:
+ return {};
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-link
+ v-if="cell.href"
+ v-gl-tooltip="cell.tooltip"
+ :href="cell.href"
+ class="gl-text-body gl-text-decoration-underline"
+ >
+ {{ cell.text }}
+ </gl-link>
+ <span v-else>{{ cell.text }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
index dde5a5a4a05..75afb7a00bc 100644
--- a/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
+++ b/app/assets/javascripts/runner/components/runner_bulk_delete_checkbox.vue
@@ -1,5 +1,6 @@
<script>
import { GlFormCheckbox } from '@gitlab/ui';
+import { s__ } from '~/locale';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
export default {
@@ -25,14 +26,20 @@ export default {
},
},
computed: {
+ deletableRunners() {
+ return this.runners.filter((runner) => runner.userPermissions?.deleteRunner);
+ },
disabled() {
- return !this.runners.length;
+ return !this.deletableRunners.length;
},
checked() {
- return Boolean(this.runners.length) && this.runners.every(this.isChecked);
+ return Boolean(this.deletableRunners.length) && this.deletableRunners.every(this.isChecked);
},
indeterminate() {
- return !this.checked && this.runners.some(this.isChecked);
+ return !this.checked && this.deletableRunners.some(this.isChecked);
+ },
+ label() {
+ return this.checked ? s__('Runners|Unselect all') : s__('Runners|Select all');
},
},
methods: {
@@ -41,7 +48,7 @@ export default {
},
onChange($event) {
this.localMutations.setRunnersChecked({
- runners: this.runners,
+ runners: this.deletableRunners,
isChecked: $event,
});
},
@@ -51,6 +58,7 @@ export default {
<template>
<gl-form-checkbox
+ :aria-label="label"
:indeterminate="indeterminate"
:checked="checked"
:disabled="disabled"
diff --git a/app/assets/javascripts/runner/components/runner_delete_button.vue b/app/assets/javascripts/runner/components/runner_delete_button.vue
index 62382891df0..b4f022a7d14 100644
--- a/app/assets/javascripts/runner/components/runner_delete_button.vue
+++ b/app/assets/javascripts/runner/components/runner_delete_button.vue
@@ -5,12 +5,7 @@ import { createAlert } from '~/flash';
import { sprintf } from '~/locale';
import { captureException } from '~/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import {
- I18N_DELETE_DISABLED_MANY_PROJECTS,
- I18N_DELETE_DISABLED_UNKNOWN_REASON,
- I18N_DELETE_RUNNER,
- I18N_DELETED_TOAST,
-} from '../constants';
+import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants';
import RunnerDeleteModal from './runner_delete_modal.vue';
export default {
@@ -31,11 +26,6 @@ export default {
return runner?.id && runner?.shortSha;
},
},
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
compact: {
type: Boolean,
required: false,
@@ -85,29 +75,14 @@ export default {
return null;
},
tooltip() {
- if (this.disabled && this.runner.projectCount > 1) {
- return I18N_DELETE_DISABLED_MANY_PROJECTS;
- }
- if (this.disabled) {
- return I18N_DELETE_DISABLED_UNKNOWN_REASON;
- }
-
// Only show basic "delete" tooltip when compact.
// Also prevent a "sticky" tooltip: If this button is
- // disabled, mouseout listeners don't run leaving the tooltip stuck
+ // loading, mouseout listeners don't run leaving the tooltip stuck
if (this.compact && !this.deleting) {
return I18N_DELETE_RUNNER;
}
return '';
},
- wrapperTabindex() {
- if (this.disabled) {
- // Trigger tooltip on keyboard-focusable wrapper
- // See https://bootstrap-vue.org/docs/directives/tooltip
- return '0';
- }
- return null;
- },
},
methods: {
async onDelete() {
@@ -156,14 +131,13 @@ export default {
</script>
<template>
- <div v-gl-tooltip="tooltip" class="btn-group" :tabindex="wrapperTabindex">
+ <div v-gl-tooltip="tooltip" class="btn-group">
<gl-button
v-gl-modal="runnerDeleteModalId"
:aria-label="ariaLabel"
:icon="icon"
:class="buttonClass"
:loading="deleting"
- :disabled="disabled"
variant="danger"
category="secondary"
v-bind="$attrs"
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index 79f934764c6..3d72abcd393 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -4,7 +4,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { ACCESS_LEVEL_REF_PROTECTED, GROUP_TYPE, PROJECT_TYPE } from '../constants';
import RunnerDetail from './runner_detail.vue';
@@ -29,7 +28,6 @@ export default {
RunnerTags,
TimeAgo,
},
- mixins: [glFeatureFlagMixin()],
props: {
runner: {
type: Object,
@@ -117,10 +115,7 @@ export default {
</template>
</runner-detail>
<runner-detail :label="s__('Runners|Maximum job timeout')" :value="maximumTimeout" />
- <runner-detail
- v-if="glFeatures.enforceRunnerTokenExpiresAt"
- :empty-value="s__('Runners|Never expires')"
- >
+ <runner-detail :empty-value="s__('Runners|Never expires')">
<template #label>
{{ s__('Runners|Token expiry') }}
<help-popover :options="tokenExpirationHelpPopoverOptions">
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index 5a9ab21a457..da59de9a9eb 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -85,7 +85,6 @@ export default {
</script>
<template>
<filtered-search
- class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1"
v-bind="$attrs"
:namespace="namespace"
recent-searches-storage-key="runners-search"
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index 26f1f3ce08c..e895537dcdc 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -2,15 +2,20 @@
import { GlFormCheckbox, GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
import { formatJobCount, tableField } from '../utils';
+import RunnerBulkDelete from './runner_bulk_delete.vue';
+import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue';
import RunnerStatusPopover from './runner_status_popover.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
+import RunnerOwnerCell from './cells/runner_owner_cell.vue';
const defaultFields = [
tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }),
tableField({ key: 'summary', label: s__('Runners|Runner') }),
+ tableField({ key: 'owner', label: s__('Runners|Owner'), thClasses: ['gl-w-20p'] }),
tableField({ key: 'actions', label: '', thClasses: ['gl-w-15p'] }),
];
@@ -19,9 +24,13 @@ export default {
GlFormCheckbox,
GlTableLite,
GlSkeletonLoader,
+ HelpPopover,
+ RunnerBulkDelete,
+ RunnerBulkDeleteCheckbox,
RunnerStatusPopover,
RunnerStackedSummaryCell,
RunnerStatusCell,
+ RunnerOwnerCell,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -34,6 +43,7 @@ export default {
},
},
},
+ inject: ['localMutations'],
props: {
checkable: {
type: Boolean,
@@ -50,7 +60,7 @@ export default {
required: true,
},
},
- emits: ['checked'],
+ emits: ['deleted'],
data() {
return { checkedRunnerIds: [] };
},
@@ -79,6 +89,12 @@ export default {
},
},
methods: {
+ canDelete(runner) {
+ return runner.userPermissions?.deleteRunner;
+ },
+ onDeleted(event) {
+ this.$emit('deleted', event);
+ },
formatJobCount(jobCount) {
return formatJobCount(jobCount);
},
@@ -91,7 +107,7 @@ export default {
return {};
},
onCheckboxChange(runner, isChecked) {
- this.$emit('checked', {
+ this.localMutations.setRunnerChecked({
runner,
isChecked,
});
@@ -104,6 +120,7 @@ export default {
</script>
<template>
<div>
+ <runner-bulk-delete v-if="checkable" :runners="runners" @deleted="onDeleted" />
<gl-table-lite
:aria-busy="loading"
:class="tableClass"
@@ -116,11 +133,15 @@ export default {
fixed
>
<template #head(checkbox)>
- <slot name="head-checkbox"></slot>
+ <runner-bulk-delete-checkbox :runners="runners" />
</template>
<template #cell(checkbox)="{ item }">
- <gl-form-checkbox :checked="isChecked(item)" @change="onCheckboxChange(item, $event)" />
+ <gl-form-checkbox
+ v-if="canDelete(item)"
+ :checked="isChecked(item)"
+ @change="onCheckboxChange(item, $event)"
+ />
</template>
<template #head(status)="{ label }">
@@ -140,6 +161,21 @@ export default {
</runner-stacked-summary-cell>
</template>
+ <template #head(owner)="{ label }">
+ {{ label }}
+ <help-popover>
+ {{
+ s__(
+ 'Runners|The project, group or instance where the runner was registered. Instance runners are always owned by Administrator.',
+ )
+ }}
+ </help-popover>
+ </template>
+
+ <template #cell(owner)="{ item }">
+ <runner-owner-cell :runner="item" />
+ </template>
+
<template #cell(actions)="{ item }">
<slot name="runner-actions-cell" :runner="item"></slot>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/runner/components/runner_list_empty_state.vue
index ab9cde6a401..e6576c83e69 100644
--- a/app/assets/javascripts/runner/components/runner_list_empty_state.vue
+++ b/app/assets/javascripts/runner/components/runner_list_empty_state.vue
@@ -53,7 +53,7 @@ export default {
:svg-path="svgPath"
:svg-height="$options.svgHeight"
>
- <template #description>
+ <template v-if="registrationToken" #description>
<gl-sprintf
:message="
s__(
@@ -71,5 +71,12 @@ export default {
:registration-token="registrationToken"
/>
</template>
+ <template v-else #description>
+ {{
+ s__(
+ 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.',
+ )
+ }}
+ </template>
</gl-empty-state>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_membership_toggle.vue b/app/assets/javascripts/runner/components/runner_membership_toggle.vue
new file mode 100644
index 00000000000..2b37b1cc797
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_membership_toggle.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlToggle } from '@gitlab/ui';
+import {
+ I18N_SHOW_ONLY_INHERITED,
+ MEMBERSHIP_DESCENDANTS,
+ MEMBERSHIP_ALL_AVAILABLE,
+} from '../constants';
+
+export default {
+ components: {
+ GlToggle,
+ },
+ props: {
+ value: {
+ type: String,
+ default: MEMBERSHIP_DESCENDANTS,
+ required: false,
+ },
+ },
+ computed: {
+ toggle() {
+ return this.value === MEMBERSHIP_DESCENDANTS;
+ },
+ },
+ methods: {
+ onChange(value) {
+ this.$emit('input', value ? MEMBERSHIP_DESCENDANTS : MEMBERSHIP_ALL_AVAILABLE);
+ },
+ },
+ I18N_SHOW_ONLY_INHERITED,
+};
+</script>
+
+<template>
+ <gl-toggle
+ data-testid="runner-membership-toggle"
+ :value="toggle"
+ :label="$options.I18N_SHOW_ONLY_INHERITED"
+ label-position="left"
+ @change="onChange"
+ />
+</template>
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
index 59230bb809e..6e7c41885f8 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -7,6 +7,12 @@ import { s__ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { RUNNER_TAG_BG_CLASS } from '../../constants';
+// TODO This should be implemented via a GraphQL API
+// The API should
+// 1) scope to the rights of the user
+// 2) stay up to date to the removal of old tags
+// 3) consider the scope of search, like searching within the tags of a group
+// See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
export const TAG_SUGGESTIONS_PATH = '/admin/runners/tag_list.json';
export default {
@@ -29,12 +35,6 @@ export default {
},
methods: {
getTagsOptions(search) {
- // TODO This should be implemented via a GraphQL API
- // The API should
- // 1) scope to the rights of the user
- // 2) stay up to date to the removal of old tags
- // 3) consider the scope of search, like searching within the tags of a group
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
return axios
.get(TAG_SUGGESTIONS_PATH, {
params: {
@@ -46,6 +46,12 @@ export default {
});
},
async fetchTags(searchTerm) {
+ // Note: Suggestions should only be enabled for admin users
+ if (this.config.suggestionsDisabled) {
+ this.tags = [];
+ return;
+ }
+
this.loading = true;
try {
this.tags = await this.getTagsOptions(searchTerm);
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 3009577599f..dfc5f0c4152 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -11,6 +11,9 @@ export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
+export const FILTER_CSS_CLASSES =
+ 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1';
+
// Type
export const I18N_ALL_TYPES = s__('Runners|All');
@@ -76,12 +79,6 @@ export const I18N_RESUME = __('Resume');
export const I18N_RESUME_TOOLTIP = s__('Runners|Resume accepting jobs');
export const I18N_DELETE_RUNNER = s__('Runners|Delete runner');
-export const I18N_DELETE_DISABLED_MANY_PROJECTS = s__(
- 'Runners|Multi-project runners cannot be deleted',
-);
-export const I18N_DELETE_DISABLED_UNKNOWN_REASON = s__(
- 'Runners|Runner cannot be deleted, please contact your administrator',
-);
export const I18N_DELETED_TOAST = s__('Runners|Runner %{name} was deleted');
// List
@@ -91,6 +88,8 @@ export const I18N_LOCKED_RUNNER_DESCRIPTION = s__(
export const I18N_VERSION_LABEL = s__('Runners|Version %{version}');
export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}');
export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}');
+export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited');
+export const I18N_ADMIN = s__('Runners|Administrator');
// Runner details
@@ -116,6 +115,7 @@ export const PARAM_KEY_PAUSED = 'paused';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
export const PARAM_KEY_TAG = 'tag';
export const PARAM_KEY_SEARCH = 'search';
+export const PARAM_KEY_MEMBERSHIP = 'membership';
export const PARAM_KEY_SORT = 'sort';
export const PARAM_KEY_AFTER = 'after';
@@ -148,6 +148,13 @@ export const CONTACTED_ASC = 'CONTACTED_ASC';
export const DEFAULT_SORT = CREATED_DESC;
+// CiRunnerMembershipFilter
+
+export const MEMBERSHIP_DESCENDANTS = 'DESCENDANTS';
+export const MEMBERSHIP_ALL_AVAILABLE = 'ALL_AVAILABLE';
+
+export const DEFAULT_MEMBERSHIP = MEMBERSHIP_DESCENDANTS;
+
// Local storage namespaces
export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners';
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
index 4c519b9b867..95f9dd1beb9 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql
@@ -2,6 +2,7 @@
query getGroupRunners(
$groupFullPath: ID!
+ $membership: CiRunnerMembershipFilter
$before: String
$after: String
$first: Int
@@ -9,13 +10,14 @@ query getGroupRunners(
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
+ $tagList: [String!]
$search: String
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
id # Apollo required
runners(
- membership: DESCENDANTS
+ membership: $membership
before: $before
after: $after
first: $first
@@ -23,6 +25,7 @@ query getGroupRunners(
paused: $paused
status: $status
type: $type
+ tagList: $tagList
search: $search
sort: $sort
) {
diff --git a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
index 958b4ea0dd3..e88a2c2e7e6 100644
--- a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
+++ b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql
@@ -1,5 +1,6 @@
query getGroupRunnersCount(
$groupFullPath: ID!
+ $membership: CiRunnerMembershipFilter
$paused: Boolean
$status: CiRunnerStatus
$type: CiRunnerType
@@ -9,7 +10,7 @@ query getGroupRunnersCount(
group(fullPath: $groupFullPath) {
id # Apollo required
runners(
- membership: DESCENDANTS
+ membership: $membership
paused: $paused
status: $status
type: $type
diff --git a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
index a12ba7a751a..0dff011daaa 100644
--- a/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/list/list_item_shared.fragment.graphql
@@ -16,4 +16,18 @@ fragment ListItemShared on CiRunner {
updateRunner
deleteRunner
}
+ groups(first: 1) {
+ nodes {
+ id
+ name
+ fullName
+ webUrl
+ }
+ }
+ ownerProject {
+ id
+ name
+ nameWithNamespace
+ webUrl
+ }
}
diff --git a/app/assets/javascripts/runner/graphql/list/local_state.js b/app/assets/javascripts/runner/graphql/list/local_state.js
index 154af261bba..e0477c660b4 100644
--- a/app/assets/javascripts/runner/graphql/list/local_state.js
+++ b/app/assets/javascripts/runner/graphql/list/local_state.js
@@ -20,10 +20,6 @@ import typeDefs from './typedefs.graphql';
* localMutations.setRunnerChecked( ... )
* ```
*
- * Note: Currently only in use behind a feature flag:
- * admin_runners_bulk_delete for the admin list, rollout issue:
- * https://gitlab.com/gitlab-org/gitlab/-/issues/353981
- *
* @returns {Object} An object to configure an Apollo client:
* contains cacheConfig, typeDefs, localMutations.
*/
@@ -52,16 +48,18 @@ export const createLocalState = () => {
const localMutations = {
setRunnerChecked({ runner, isChecked }) {
- checkedRunnerIdsVar({
- ...checkedRunnerIdsVar(),
- [runner.id]: isChecked,
- });
+ const { id, userPermissions } = runner;
+ if (userPermissions?.deleteRunner) {
+ checkedRunnerIdsVar({
+ ...checkedRunnerIdsVar(),
+ [id]: isChecked,
+ });
+ }
},
setRunnersChecked({ runners, isChecked }) {
- const newVal = runners.reduce(
- (acc, { id }) => ({ ...acc, [id]: isChecked }),
- checkedRunnerIdsVar(),
- );
+ const newVal = runners
+ .filter(({ userPermissions }) => userPermissions?.deleteRunner)
+ .reduce((acc, { id }) => ({ ...acc, [id]: isChecked }), checkedRunnerIdsVar());
checkedRunnerIdsVar(newVal);
},
clearChecked() {
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index 70826a6bfa1..7f56d895682 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -10,7 +10,9 @@ import {
fromSearchToVariables,
isSearchFiltered,
} from 'ee_else_ce/runner/runner_search_utils';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql';
+import groupRunnersCountQuery from 'ee_else_ce/runner/graphql/list/group_runners_count.query.graphql';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerStackedLayoutBanner from '../components/runner_stacked_layout_banner.vue';
@@ -22,14 +24,17 @@ import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
+import RunnerMembershipToggle from '../components/runner_membership_toggle.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
+import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
import {
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
PROJECT_TYPE,
I18N_FETCH_ERROR,
+ FILTER_CSS_CLASSES,
} from '../constants';
import { captureException } from '../sentry_utils';
@@ -43,11 +48,13 @@ export default {
RunnerList,
RunnerListEmptyState,
RunnerName,
+ RunnerMembershipToggle,
RunnerStats,
RunnerPagination,
RunnerTypeTabs,
RunnerActionsCell,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
props: {
registrationToken: {
@@ -126,12 +133,20 @@ export default {
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
- searchTokens() {
- return [pausedTokenConfig, statusTokenConfig, upgradeStatusTokenConfig];
- },
filteredSearchNamespace() {
return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
},
+ searchTokens() {
+ return [
+ pausedTokenConfig,
+ statusTokenConfig,
+ {
+ ...tagTokenConfig,
+ suggestionsDisabled: true,
+ },
+ upgradeStatusTokenConfig,
+ ];
+ },
isSearchFiltered() {
return isSearchFiltered(this.search);
},
@@ -159,13 +174,17 @@ export default {
editUrl(runner) {
return this.runners.urlsById[runner.id]?.edit;
},
+ refetchCounts() {
+ this.$apollo.getClient().refetchQueries({ include: [groupRunnersCountQuery] });
+ },
onToggledPaused() {
// When a runner becomes Paused, the tab count can
// become stale, refetch outdated counts.
- this.$refs['runner-type-tabs'].refetch();
+ this.refetchCounts();
},
onDeleted({ message }) {
this.$root.$toast?.show(message);
+ this.refetchCounts();
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@@ -176,6 +195,7 @@ export default {
},
TABS_RUNNER_TYPES: [GROUP_TYPE, PROJECT_TYPE],
GROUP_TYPE,
+ FILTER_CSS_CLASSES,
};
</script>
@@ -204,11 +224,21 @@ export default {
/>
</div>
- <runner-filtered-search-bar
- v-model="search"
- :tokens="searchTokens"
- :namespace="filteredSearchNamespace"
- />
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3"
+ :class="$options.FILTER_CSS_CLASSES"
+ >
+ <runner-filtered-search-bar
+ v-model="search"
+ :tokens="searchTokens"
+ :namespace="filteredSearchNamespace"
+ class="gl-flex-grow-1 gl-align-self-stretch"
+ />
+ <runner-membership-toggle
+ v-model="search.membership"
+ class="gl-align-self-end gl-md-align-self-center"
+ />
+ </div>
<runner-stats :scope="$options.GROUP_TYPE" :variables="countVariables" />
@@ -220,7 +250,7 @@ export default {
:filtered-svg-path="emptyStateFilteredSvgPath"
/>
<template v-else>
- <runner-list :runners="runners.items" :loading="runnersLoading">
+ <runner-list :runners="runners.items" :loading="runnersLoading" @deleted="onDeleted">
<template #runner-name="{ runner }">
<gl-link :href="webUrl(runner)">
<runner-name :runner="runner" />
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index feed6b0ceb7..0e7efd2b8a1 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -2,6 +2,7 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { createLocalState } from '../graphql/list/local_state';
import GroupRunnersApp from './group_runners_app.vue';
Vue.use(GlToast);
@@ -26,8 +27,10 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
emptyStateFilteredSvgPath,
} = el.dataset;
+ const { cacheConfig, typeDefs, localMutations } = createLocalState();
+
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { cacheConfig, typeDefs }),
});
return new Vue({
@@ -35,6 +38,7 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
apolloProvider,
provide: {
runnerInstallHelpPage,
+ localMutations,
groupId,
onlineContactTimeoutSecs: parseInt(onlineContactTimeoutSecs, 10),
staleTimeoutSecs: parseInt(staleTimeoutSecs, 10),
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index dc582ccbac1..adc832b0600 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -13,10 +13,12 @@ import {
PARAM_KEY_RUNNER_TYPE,
PARAM_KEY_TAG,
PARAM_KEY_SEARCH,
+ PARAM_KEY_MEMBERSHIP,
PARAM_KEY_SORT,
PARAM_KEY_AFTER,
PARAM_KEY_BEFORE,
DEFAULT_SORT,
+ DEFAULT_MEMBERSHIP,
RUNNER_PAGE_SIZE,
} from './constants';
import { getPaginationVariables } from './utils';
@@ -57,9 +59,10 @@ import { getPaginationVariables } from './utils';
* @param {Object} search
* @returns {boolean} True if the value follows the search format.
*/
-export const searchValidator = ({ runnerType, filters, sort }) => {
+export const searchValidator = ({ runnerType, membership, filters, sort }) => {
return (
(runnerType === null || typeof runnerType === 'string') &&
+ (membership === null || typeof membership === 'string') &&
Array.isArray(filters) &&
typeof sort === 'string'
);
@@ -140,9 +143,11 @@ export const updateOutdatedUrl = (url = window.location.href) => {
export const fromUrlQueryToSearch = (query = window.location.search) => {
const params = queryToObject(query, { gatherArrays: true });
const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null;
+ const membership = params[PARAM_KEY_MEMBERSHIP]?.[0] || null;
return {
runnerType,
+ membership: membership || DEFAULT_MEMBERSHIP,
filters: prepareTokens(
urlQueryToFilter(query, {
filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG],
@@ -162,13 +167,14 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
* @returns {String} New URL for the page
*/
export const fromSearchToUrl = (
- { runnerType = null, filters = [], sort = null, pagination = {} },
+ { runnerType = null, membership = null, filters = [], sort = null, pagination = {} },
url = window.location.href,
) => {
const filterParams = {
// Defaults
[PARAM_KEY_STATUS]: [],
[PARAM_KEY_RUNNER_TYPE]: [],
+ [PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
@@ -180,6 +186,10 @@ export const fromSearchToUrl = (
filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType];
}
+ if (membership && membership !== DEFAULT_MEMBERSHIP) {
+ filterParams[PARAM_KEY_MEMBERSHIP] = [membership];
+ }
+
if (!filterParams[PARAM_KEY_SEARCH]) {
filterParams[PARAM_KEY_SEARCH] = null;
}
@@ -203,6 +213,7 @@ export const fromSearchToUrl = (
*/
export const fromSearchToVariables = ({
runnerType = null,
+ membership = null,
filters = [],
sort = null,
pagination = {},
@@ -226,6 +237,9 @@ export const fromSearchToVariables = ({
if (runnerType) {
filterVariables.type = runnerType;
}
+ if (membership) {
+ filterVariables.membership = membership;
+ }
if (sort) {
filterVariables.sort = sort;
}
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index 446ab7f433c..ba12f31ef87 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,7 +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 { initSidebar } from './sidebar';
+import { initSidebar, sidebarInitState } from './sidebar';
import { initSearchSort } from './sort';
import createStore from './store';
import { initTopbar } from './topbar';
@@ -9,14 +9,18 @@ import { initBlobRefSwitcher } from './under_topbar';
export const initSearchApp = () => {
const query = queryToObject(window.location.search);
+ const navigation = sidebarInitState();
- const store = createStore({ query });
+ const store = createStore({ query, navigation });
initTopbar(store);
initSidebar(store);
initSearchSort(store);
setHighlightClass(query.search); // Code Highlighting
- refreshCounts(); // Other Scope Tab Counts
initBlobRefSwitcher(); // Code Search Branch Picker
+
+ if (!gon.features?.searchPageVerticalNav) {
+ refreshCounts(); // Other Scope Tab Counts
+ }
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 5c7cbeac5b2..789efc8f09d 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -17,6 +17,9 @@ export default {
showReset() {
return this.urlQuery.state || this.urlQuery.confidential;
},
+ showSidebar() {
+ return this.urlQuery.scope === 'issues' || this.urlQuery.scope === 'merge_requests';
+ },
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@@ -29,15 +32,17 @@ export default {
class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5"
@submit.prevent="applyQuery"
>
- <status-filter />
- <confidentiality-filter />
- <div class="gl-display-flex gl-align-items-center gl-mt-3">
- <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
- {{ __('Apply') }}
- </gl-button>
- <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
- __('Reset filters')
- }}</gl-link>
- </div>
+ <template v-if="showSidebar">
+ <status-filter />
+ <confidentiality-filter />
+ <div class="gl-display-flex gl-align-items-center gl-mt-3">
+ <gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
+ {{ __('Apply') }}
+ </gl-button>
+ <gl-link v-if="showReset" class="gl-ml-auto" @click="resetQuery">{{
+ __('Reset filters')
+ }}</gl-link>
+ </div>
+ </template>
</form>
</template>
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index 1414adcac27..c6b1257c4ef 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -4,6 +4,15 @@ import GlobalSearchSidebar from './components/app.vue';
Vue.use(Translate);
+export const sidebarInitState = () => {
+ const el = document.getElementById('js-search-sidebar');
+
+ if (!el) return {};
+
+ const { navigation } = el.dataset;
+ return JSON.parse(navigation);
+};
+
export const initSidebar = (store) => {
const el = document.getElementById('js-search-sidebar');
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index dc8b6201953..be5742e5949 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY, SIDEBAR_PARAMS } from './constants';
@@ -13,7 +13,7 @@ export const fetchGroups = ({ commit }, search) => {
commit(types.RECEIVE_GROUPS_SUCCESS, data);
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching groups.') });
+ createAlert({ message: __('There was a problem fetching groups.') });
commit(types.RECEIVE_GROUPS_ERROR);
});
};
@@ -23,7 +23,7 @@ export const fetchProjects = ({ commit, state }, search) => {
const groupId = state.query?.group_id;
const handleCatch = () => {
- createFlash({ message: __('There was an error fetching projects') });
+ createAlert({ message: __('There was an error fetching projects') });
commit(types.RECEIVE_PROJECTS_ERROR);
};
const handleSuccess = ({ data }) => {
@@ -59,7 +59,7 @@ export const loadFrequentGroups = async ({ commit, state }) => {
const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
- createFlash({ message: __('There was a problem fetching recent groups.') });
+ createAlert({ message: __('There was a problem fetching recent groups.') });
}
};
@@ -70,7 +70,7 @@ export const loadFrequentProjects = async ({ commit, state }) => {
const inflatedData = mergeById(await Promise.all(promises), storedData);
commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData });
} catch {
- createFlash({ message: __('There was a problem fetching recent projects.') });
+ createAlert({ message: __('There was a problem fetching recent projects.') });
}
};
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index f27dae8249d..d0fcbb0d83b 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -1,7 +1,9 @@
<script>
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
+import { parseBoolean } from '~/lib/utils/common_utils';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
@@ -16,6 +18,7 @@ export default {
GroupFilter,
ProjectFilter,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
groupInitialData: {
type: Object,
@@ -39,7 +42,10 @@ export default {
},
},
showFilters() {
- return !this.query.snippets || this.query.snippets === 'false';
+ return !parseBoolean(this.query.snippets);
+ },
+ hasVerticalNav() {
+ return this.glFeatures.searchPageVerticalNav;
},
},
created() {
@@ -52,24 +58,27 @@ export default {
</script>
<template>
- <section class="search-page-form gl-lg-display-flex gl-align-items-flex-end">
- <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
- <label>{{ $options.i18n.searchLabel }}</label>
- <gl-search-box-by-click
- id="dashboard_search"
- v-model="search"
- name="search"
- :placeholder="$options.i18n.searchPlaceholder"
- @submit="applyQuery"
- />
- </div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Group') }}</label>
- <group-filter :initial-data="groupInitialData" />
- </div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Project') }}</label>
- <project-filter :initial-data="projectInitialData" />
+ <section class="search-page-form gl-lg-display-flex gl-flex-direction-column">
+ <div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end">
+ <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
+ <label>{{ $options.i18n.searchLabel }}</label>
+ <gl-search-box-by-click
+ id="dashboard_search"
+ v-model="search"
+ name="search"
+ :placeholder="$options.i18n.searchPlaceholder"
+ @submit="applyQuery"
+ />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Group') }}</label>
+ <group-filter :initial-data="groupInitialData" />
+ </div>
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
+ <label class="gl-display-block">{{ __('Project') }}</label>
+ <project-filter :initial-data="projectInitialData" />
+ </div>
</div>
+ <hr v-if="hasVerticalNav" class="gl-mt-5 gl-mb-0 gl-border-gray-100" />
</section>
</template>
diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue
index e9cc9616fd0..3fc279f363a 100644
--- a/app/assets/javascripts/search_settings/components/search_settings.vue
+++ b/app/assets/javascripts/search_settings/components/search_settings.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSearchBoxByType } from '@gitlab/ui';
+import { GlEmptyState, GlSearchBoxByType } from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import {
EXCLUDED_NODES,
@@ -96,6 +96,8 @@ const displayResults = ({ sectionSelector, expandSection, searchTerm }, matching
hideSectionsExcept(sectionSelector, sections);
sections.forEach(expandSection);
highlightText(matchingTextNodes, searchTerm);
+
+ return sections.length > 0;
};
const clearResults = (params) => {
@@ -126,6 +128,7 @@ const search = (root, searchTerm) => {
export default {
components: {
+ GlEmptyState,
GlSearchBoxByType,
},
props: {
@@ -137,6 +140,11 @@ export default {
type: String,
required: true,
},
+ hideWhenEmptySelector: {
+ type: String,
+ required: true,
+ default: null,
+ },
isExpandedFn: {
type: Function,
required: false,
@@ -147,8 +155,16 @@ export default {
data() {
return {
searchTerm: '',
+ hasMatches: true,
};
},
+ watch: {
+ hasMatches(newHasMatches) {
+ document.querySelectorAll(this.hideWhenEmptySelector).forEach((section) => {
+ section.classList.toggle(HIDE_CLASS, !newHasMatches);
+ });
+ },
+ },
methods: {
search(value) {
this.searchTerm = value;
@@ -161,11 +177,12 @@ export default {
};
clearResults(displayOptions);
+ this.hasMatches = true;
if (value.length) {
saveExpansionState(document.querySelectorAll(this.sectionSelector), displayOptions);
- displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
+ this.hasMatches = displayResults(displayOptions, search(this.searchRoot, this.searchTerm));
} else {
restoreExpansionState(displayOptions);
}
@@ -181,10 +198,18 @@ export default {
};
</script>
<template>
- <gl-search-box-by-type
- :value="searchTerm"
- :debounce="$options.TYPING_DELAY"
- :placeholder="__('Search page')"
- @input="search"
- />
+ <div>
+ <gl-search-box-by-type
+ :value="searchTerm"
+ :debounce="$options.TYPING_DELAY"
+ :placeholder="__('Search page')"
+ @input="search"
+ />
+
+ <gl-empty-state
+ v-if="!hasMatches"
+ :title="__('No results found')"
+ :description="__('Edit your search and try again')"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/search_settings/mount.js b/app/assets/javascripts/search_settings/mount.js
index b1086f9ca1f..b727b55781a 100644
--- a/app/assets/javascripts/search_settings/mount.js
+++ b/app/assets/javascripts/search_settings/mount.js
@@ -11,6 +11,7 @@ const mountSearch = ({ el }) =>
props: {
searchRoot: document.querySelector('#content-body'),
sectionSelector: '.js-search-settings-section, section.settings',
+ hideWhenEmptySelector: '.js-hide-when-nothing-matches-search',
isExpandedFn: isExpanded,
},
on: {
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index ecde9235e93..7828efc358a 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -21,7 +21,9 @@ export const i18n = {
),
description: s__(
`SecurityConfiguration|Once you've enabled a scan for the default branch,
- any subsequent feature branch you create will include the scan.`,
+ any subsequent feature branch you create will include the scan. An enabled
+ scanner will not be reflected as such until the pipeline has been
+ successfully executed and it has generated valid artifacts.`,
),
securityConfiguration: __('Security Configuration'),
vulnerabilityManagement: s__('SecurityConfiguration|Vulnerability Management'),
@@ -165,7 +167,12 @@ export default {
</template>
</user-callout-dismisser>
- <gl-tabs content-class="gl-pt-0" sync-active-tab-with-query-params lazy>
+ <gl-tabs
+ content-class="gl-pt-0"
+ data-qa-selector="security_configuration_container"
+ sync-active-tab-with-query-params
+ lazy
+ >
<gl-tab
data-testid="security-testing-tab"
:title="$options.i18n.securityTesting"
diff --git a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
index 891d7bf2eb0..eaff1ce6055 100644
--- a/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
+++ b/app/assets/javascripts/security_configuration/components/upgrade_banner.vue
@@ -22,7 +22,7 @@ export default {
s__('SecurityConfiguration|High-level vulnerability statistics across projects and groups'),
s__('SecurityConfiguration|Runtime security metrics for application environments'),
s__(
- 'SecurityConfiguration|More scan types, including Container Scanning, DAST, Dependency Scanning, Fuzzing, and Licence Compliance',
+ 'SecurityConfiguration|More scan types, including DAST, Dependency Scanning, Fuzzing, and Licence Compliance',
),
],
buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'),
diff --git a/app/assets/javascripts/service_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
index f145a1b30db..f2c3f28cefa 100644
--- a/app/assets/javascripts/service_ping_consent.js
+++ b/app/assets/javascripts/service_ping_consent.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import createFlash, { hideFlash } from './flash';
+import { createAlert, hideFlash } from './flash';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
@@ -27,7 +27,7 @@ export default () => {
})
.catch(() => {
hideConsentMessage();
- createFlash({
+ createAlert({
message: __('Something went wrong. Try again later.'),
});
});
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 7f9a30b7ff1..86049a2b781 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -131,9 +131,9 @@ export default {
i18n: {
statusMessagePlaceholder: s__(`SetStatusModal|What's your status?`),
clearStatusButtonLabel: s__('SetStatusModal|Clear status'),
- availabilityCheckboxLabel: s__('SetStatusModal|Busy'),
+ availabilityCheckboxLabel: s__('SetStatusModal|Set yourself as busy'),
availabilityCheckboxHelpText: s__(
- 'SetStatusModal|An indicator appears next to your name and avatar',
+ 'SetStatusModal|Displays that you are busy or not able to respond',
),
clearStatusAfterDropdownLabel: s__('SetStatusModal|Clear status after'),
clearStatusAfterMessage: s__('SetStatusModal|Your status resets on %{date}.'),
@@ -161,11 +161,7 @@ export default {
@click="handleEmojiClick"
>
<template #button-content>
- <span
- v-if="noEmoji"
- class="no-emoji-placeholder position-relative"
- data-testid="no-emoji-placeholder"
- >
+ <span v-if="noEmoji" class="gl-relative" data-testid="no-emoji-placeholder">
<gl-icon name="slight-smile" class="award-control-icon-neutral" />
<gl-icon name="smiley" class="award-control-icon-positive" />
<gl-icon name="smile" class="award-control-icon-super-positive" />
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 80b1cb8c4d5..80158c55dbc 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
@@ -1,7 +1,7 @@
<script>
import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
@@ -89,7 +89,7 @@ export default {
window.location.reload();
},
onUpdateFail() {
- createFlash({
+ createAlert({
message: s__(
"SetStatusModal|Sorry, we weren't able to set your status. Please try again later.",
),
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 18b26c7d8bd..15fd365b4da 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,6 +1,6 @@
<script>
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
@@ -113,7 +113,7 @@ export default {
})
.catch(() => {
this.loading = false;
- return createFlash({
+ return createAlert({
message: __('Error occurred when saving assignees'),
});
});
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 26fda2a823c..395dcf73693 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, n__ } from '~/locale';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
@@ -111,7 +111,7 @@ export default {
}
},
error() {
- createFlash({ message: __('An error occurred while fetching participants.') });
+ createAlert({ message: __('An error occurred while fetching participants.') });
},
},
},
@@ -191,7 +191,7 @@ export default {
return data;
})
.catch(() => {
- createFlash({ message: __('An error occurred while updating assignees.') });
+ createAlert({ message: __('An error occurred while updating assignees.') });
})
.finally(() => {
this.isSettingAssignees = false;
@@ -220,7 +220,7 @@ export default {
this.$refs.userSelect.showDropdown();
},
showError() {
- createFlash({ message: __('An error occurred while fetching participants.') });
+ createAlert({ message: __('An error occurred while fetching participants.') });
},
setDirtyState() {
this.isDirty = true;
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
index 0ed40f56bea..29298ef7627 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue
@@ -58,7 +58,7 @@ export default {
v-if="hasCannotMergeIcon"
name="warning-solid"
aria-hidden="true"
- class="merge-icon gl-left-6 gl-bottom-0"
+ class="merge-icon"
:size="12"
/>
</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
index c44ce8b0057..3532b75b6e7 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -1,6 +1,6 @@
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import { confidentialityQueries } from '~/sidebar/constants';
@@ -92,7 +92,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
} else {
@@ -101,7 +101,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
index f234c5ea3c9..f3bd58c11d4 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -1,7 +1,7 @@
<script>
import produce from 'immer';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { confidentialityQueries, Tracking } from '~/sidebar/constants';
@@ -72,7 +72,7 @@ export default {
this.$emit('confidentialityUpdated', data.workspace?.issuable?.confidential);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} confidentiality.'),
{
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index 67f36f65b5d..81090bfa062 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
@@ -41,7 +41,7 @@ export default {
return data?.issue?.customerRelationsContacts?.nodes;
},
error(error) {
- createFlash({
+ createAlert({
message: __('Something went wrong trying to load issue contacts.'),
error,
captureError: true,
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index ef99d540c86..98468583992 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
@@ -92,7 +92,7 @@ export default {
this.$emit(`${this.dateType}Updated`, data.workspace?.issuable?.[this.dateType]);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} %{dateType} date.'),
{
@@ -205,7 +205,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
} else {
@@ -214,7 +214,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} %{dateType} date.'),
{
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 8145506f32c..df03af346c0 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import eventHub from '../../event_hub';
@@ -52,7 +52,7 @@ export default {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
- createFlash({
+ createAlert({
message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
});
})
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index 286bd50f6dd..d32d8a7b044 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitl
import { mapGetters, mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import eventHub from '~/sidebar/event_hub';
import toast from '~/vue_shared/plugins/global_toast';
import EditForm from './edit_form.vue';
@@ -95,7 +95,7 @@ export default {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
- createFlash({
+ createAlert({
message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
});
})
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 933b9b11b40..55bb214aa65 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -43,6 +43,7 @@ export default {
data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
+ data-qa-selector="reviewers_edit_button"
>
{{ __('Edit') }}
</a>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index b0d820ddd15..ad061dd2e6b 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -3,7 +3,7 @@
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import Vue from 'vue';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
@@ -64,7 +64,7 @@ export default {
this.initialLoading = false;
},
error() {
- createFlash({ message: __('An error occurred while fetching reviewers.') });
+ createAlert({ message: __('An error occurred while fetching reviewers.') });
},
},
},
@@ -85,7 +85,7 @@ export default {
return this.loading || this.$apollo.queries.issuable.loading;
},
canUpdate() {
- return this.issuable.userPermissions?.updateMergeRequest || false;
+ return this.issuable.userPermissions?.adminMergeRequest || false;
},
},
created() {
@@ -120,7 +120,7 @@ export default {
})
.catch(() => {
this.loading = false;
- return createFlash({
+ return createAlert({
message: __('Error occurred when saving reviewers'),
});
});
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index a562df4ecd6..f02e0c783e1 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -7,7 +7,7 @@ import {
GlSprintf,
GlButton,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
import SeverityToken from './severity.vue';
@@ -123,7 +123,7 @@ export default {
this.severity = severity;
})
.catch(() =>
- createFlash({
+ createAlert({
message: `${this.$options.i18n.UPDATE_SEVERITY_ERROR} ${this.$options.i18n.TRY_AGAIN}`,
}),
)
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 6c615109bb8..c33b1468ca4 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -13,7 +13,7 @@ import {
GlButton,
} from '@gitlab/ui';
import { kebabCase, snakeCase } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
@@ -25,6 +25,8 @@ import {
Tracking,
IssuableAttributeState,
IssuableAttributeType,
+ LocalizedIssuableAttributeType,
+ IssuableAttributeTypeKeyMap,
issuableAttributesQueries,
noAttributeId,
defaultEpicSort,
@@ -125,7 +127,7 @@ export default {
return data?.workspace?.issuable.attribute;
},
error(error) {
- createFlash({
+ createAlert({
message: this.i18n.currentFetchError,
captureError: true,
error,
@@ -179,7 +181,7 @@ export default {
return [];
},
error(error) {
- createFlash({ message: this.i18n.listFetchError, captureError: true, error });
+ createAlert({ message: this.i18n.listFetchError, captureError: true, error });
},
},
},
@@ -229,7 +231,9 @@ export default {
return timeFor(this.currentAttribute?.dueDate);
},
i18n() {
- return dropdowni18nText(this.issuableAttribute, this.issuableType);
+ const localizedAttribute =
+ LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]];
+ return dropdowni18nText(localizedAttribute, this.issuableType);
},
isEpic() {
// MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
@@ -280,7 +284,7 @@ export default {
})
.then(({ data }) => {
if (data.issuableSetAttribute?.errors?.length) {
- createFlash({
+ createAlert({
message: data.issuableSetAttribute.errors[0],
captureError: true,
error: data.issuableSetAttribute.errors[0],
@@ -290,7 +294,7 @@ export default {
}
})
.catch((error) => {
- createFlash({ message: this.i18n.updateError, captureError: true, error });
+ createAlert({ message: this.i18n.updateError, captureError: true, error });
})
.finally(() => {
this.updating = false;
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index cc88812c7b0..1680e42e5e4 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -89,7 +89,9 @@ export default {
return;
}
- this.edit = true;
+ if (this.canEdit && this.canUpdate) {
+ this.edit = true;
+ }
this.$emit('open');
window.addEventListener('click', this.collapseWhenOffClick);
window.addEventListener('keyup', this.collapseOnEscape);
@@ -125,7 +127,7 @@ export default {
<template>
<div>
<div
- class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"
+ class="gl-display-flex gl-align-items-center gl-line-height-20 gl-text-gray-900 gl-font-weight-bold"
@click.self="collapse"
>
<span class="hide-collapsed" data-testid="title" @click="collapse">
diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
index e5bee4df9b8..99e7c825b72 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -1,6 +1,6 @@
<script>
-import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
@@ -22,6 +22,7 @@ export default {
GlTooltip: GlTooltipDirective,
},
components: {
+ GlDropdownForm,
GlIcon,
GlLoadingIcon,
GlToggle,
@@ -73,7 +74,7 @@ export default {
this.$emit('subscribedUpdated', data.workspace?.issuable?.subscribed);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
@@ -137,7 +138,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
}
@@ -148,7 +149,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(
__('Something went wrong while setting %{issuableType} notifications.'),
{
@@ -181,7 +182,7 @@ export default {
</script>
<template>
- <div v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <gl-dropdown-form v-if="isMergeRequest" class="gl-new-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
@@ -192,7 +193,7 @@ export default {
@change="toggleSubscribed"
/>
</div>
- </div>
+ </gl-dropdown-form>
<sidebar-editable-item
v-else
ref="editable"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index d751816bd94..124464088cf 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlTableLite, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
@@ -47,7 +47,7 @@ export default {
return this.extractTimelogs(data);
},
error() {
- createFlash({ message: __('Something went wrong. Please try again.') });
+ createAlert({ message: __('Something went wrong. Please try again.') });
},
},
},
@@ -105,7 +105,7 @@ export default {
}
})
.catch((error) => {
- createFlash({
+ createAlert({
message: s__('TimeTracking|An error occurred while removing the timelog.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index d472b67d976..62b05421884 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -61,7 +61,7 @@ export default {
</script>
<template>
- <div class="block">
+ <div class="block time-tracking">
<issuable-time-tracker
:full-path="fullPath"
:issuable-id="issuableId"
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index 42e16aae312..5da2d65723a 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
@@ -73,7 +73,7 @@ export default {
this.$emit('todoUpdated', currentUserTodos.length > 0);
},
error() {
- createFlash({
+ createAlert({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
@@ -155,7 +155,7 @@ export default {
},
}) => {
if (errors.length) {
- createFlash({
+ createAlert({
message: errors[0],
});
}
@@ -166,7 +166,7 @@ export default {
},
)
.catch(() => {
- createFlash({
+ createAlert({
message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
issuableType: this.issuableType,
}),
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 60cb4cff727..6248bcb8e2d 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,3 +1,4 @@
+import { invert } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
@@ -251,6 +252,12 @@ export const IssuableAttributeType = {
Milestone: 'milestone',
};
+export const LocalizedIssuableAttributeType = {
+ Milestone: s__('Issuable|milestone'),
+};
+
+export const IssuableAttributeTypeKeyMap = invert(IssuableAttributeType);
+
export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 5a3122e83d0..2cce27df598 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { escape } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
function isValidProjectId(id) {
@@ -44,7 +44,7 @@ class SidebarMoveIssue {
.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() =>
- createFlash({
+ createAlert({
message: __('An error occurred while fetching projects autocomplete.'),
}),
);
@@ -79,7 +79,7 @@ class SidebarMoveIssue {
this.$confirmButton.disable().addClass('is-loading');
this.mediator.moveIssue().catch(() => {
- createFlash({ message: __('An error occurred while moving the issue.') });
+ createAlert({ message: __('An error occurred while moving the issue.') });
this.$confirmButton.enable().removeClass('is-loading');
});
}
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 1cb3c30b9e0..9b5bad710dd 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -161,7 +161,7 @@ function mountAssigneesComponent() {
fullPath,
issuableType,
issuableId: id,
- allowMultipleAssignees: !el.dataset.maxAssignees,
+ allowMultipleAssignees: !el.dataset.maxAssignees || el.dataset.maxAssignees > 1,
editable,
},
scopedSlots: {
diff --git a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
index d665ca1e084..71ce58fb9cc 100644
--- a/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/epic_participants.query.graphql
@@ -1,5 +1,4 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
-#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
query epicParticipants($fullPath: ID!, $iid: ID) {
workspace: group(fullPath: $fullPath) {
@@ -9,7 +8,6 @@ query epicParticipants($fullPath: ID!, $iid: ID) {
participants {
nodes {
...User
- ...UserAvailability
}
}
}
diff --git a/app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql b/app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql
deleted file mode 100644
index 90d1a7794ea..00000000000
--- a/app/assets/javascripts/sidebar/queries/sidebar_details.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query sidebarDetails($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- issue(iid: $iid) {
- id
- iid
- }
- }
-}
diff --git a/app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql b/app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql
deleted file mode 100644
index 0505f88773d..00000000000
--- a/app/assets/javascripts/sidebar/queries/sidebar_details_mr.query.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-query mergeRequestSidebarDetails($fullPath: ID!, $iid: String!) {
- project(fullPath: $fullPath) {
- id
- mergeRequest(iid: $iid) {
- id
- iid # currently unused.
- }
- }
-}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index beacdeb559c..00d3177b75a 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,15 +1,8 @@
-import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebar_details.query.graphql';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
-import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.graphql';
-
-const queries = {
- merge_request: sidebarDetailsMRQuery,
- issue: sidebarDetailsIssueQuery,
-};
export const gqClient = createGqClient(
{},
@@ -36,37 +29,13 @@ export default class SidebarService {
}
get() {
- return Promise.all([
- axios.get(this.endpoint),
- gqClient.query({
- query: this.sidebarDetailsQuery(),
- variables: {
- fullPath: this.fullPath,
- iid: this.iid.toString(),
- },
- }),
- ]);
- }
-
- sidebarDetailsQuery() {
- return queries[this.issuableType];
+ return axios.get(this.endpoint);
}
update(key, data) {
return axios.put(this.endpoint, { [key]: data });
}
- updateWithGraphQl(mutation, variables) {
- return gqClient.mutate({
- mutation,
- variables: {
- ...variables,
- projectPath: this.fullPath,
- iid: this.iid.toString(),
- },
- });
- }
-
getProjectsAutocomplete(searchTerm) {
return axios.get(this.projectsAutocompleteEndpoint, {
params: {
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index f7c93b6903c..912f0fdcbef 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,5 +1,5 @@
import Store from '~/sidebar/stores/sidebar_store';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { visitUrl } from '../lib/utils/url_utility';
@@ -93,11 +93,11 @@ export default class SidebarMediator {
fetch() {
return this.service
.get()
- .then(([restResponse, graphQlResponse]) => {
- this.processFetchedData(restResponse.data, graphQlResponse.data);
+ .then(({ data }) => {
+ this.processFetchedData(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error occurred when fetching sidebar data'),
}),
);
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 26838682fc8..6e5b2ce4dbe 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,10 +1,10 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
+import { createAlert } from '~/flash';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { spriteIcon } from '~/lib/utils/common_utils';
import FilesCommentButton from './files_comment_button';
-import createFlash from './flash';
import initImageDiffHelper from './image_diff/helpers/init_image_diff';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@@ -96,7 +96,7 @@ export default class SingleFileDiff {
if (cb) cb();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('An error occurred while retrieving diff'),
});
});
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index 2537ec78850..4a7528d9c8e 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -2,7 +2,7 @@
import { GlButton, GlLoadingIcon, GlFormInput, GlFormGroup } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@@ -145,7 +145,7 @@ export default {
const defaultErrorMsg = this.newSnippet
? SNIPPET_CREATE_MUTATION_ERROR
: SNIPPET_UPDATE_MUTATION_ERROR;
- createFlash({
+ createAlert({
message: sprintf(defaultErrorMsg, { err }),
});
this.isUpdating = false;
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index fe169775f96..7e80928cbea 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
@@ -63,7 +63,7 @@ export default {
.catch((e) => this.flashAPIFailure(e));
},
flashAPIFailure(err) {
- createFlash({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
+ createAlert({ message: sprintf(SNIPPET_BLOB_CONTENT_FETCH_ERROR, { err }) });
},
},
};
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index 86cbc2c31b3..360ffdd34e0 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -53,7 +53,9 @@ export default {
return {
blobContent: '',
activeViewerType:
- this.blob?.richViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
+ this.blob?.richViewer && !window.location.hash?.startsWith('#LC')
+ ? RICH_BLOB_VIEWER
+ : SIMPLE_BLOB_VIEWER,
};
},
computed: {
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index dd8f2897018..759a3f31a05 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -19,7 +19,7 @@ import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_DANGER, VARIANT_SUCCESS } from '~/flash';
import DeleteSnippetMutation from '../mutations/delete_snippet.mutation.graphql';
@@ -196,12 +196,12 @@ export default {
try {
this.isSubmittingSpam = true;
await axios.post(this.reportAbusePath);
- createFlash({
+ createAlert({
message: this.$options.i18n.snippetSpamSuccess,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
} catch (error) {
- createFlash({ message: this.$options.i18n.snippetSpamFailure });
+ createAlert({ message: this.$options.i18n.snippetSpamFailure, variant: VARIANT_DANGER });
} finally {
this.isSubmittingSpam = false;
}
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 6e72d95c8e6..a7760ad5d0b 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
-import createFlash from './flash';
+import { createAlert } from '~/flash';
import axios from './lib/utils/axios_utils';
export default class TaskList {
@@ -23,7 +23,7 @@ export default class TaskList {
errorMessages = e.response.data.errors.join(' ');
}
- return createFlash({
+ return createAlert({
message: errorMessages || __('Update failed'),
});
};
diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue
index 363a9d58d65..4b91872d80d 100644
--- a/app/assets/javascripts/token_access/components/token_access.vue
+++ b/app/assets/javascripts/token_access/components/token_access.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAlert,
GlButton,
GlCard,
GlFormInput,
@@ -8,7 +9,7 @@ import {
GlSprintf,
GlToggle,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
@@ -25,6 +26,9 @@ export default {
`CICD|Select the projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more.%{linkEnd}`,
),
cardHeaderTitle: s__('CICD|Add an existing project to the scope'),
+ settingDisabledMessage: s__(
+ 'CICD|Enable feature to limit job token access to the following projects.',
+ ),
addProject: __('Add project'),
cancel: __('Cancel'),
addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'),
@@ -32,6 +36,7 @@ export default {
scopeFetchError: __('There was a problem fetching the job token scope value'),
},
components: {
+ GlAlert,
GlButton,
GlCard,
GlFormInput,
@@ -58,7 +63,7 @@ export default {
return data.project.ciCdSettings.jobTokenScopeEnabled;
},
error() {
- createFlash({ message: this.$options.i18n.scopeFetchError });
+ createAlert({ message: this.$options.i18n.scopeFetchError });
},
},
projects: {
@@ -72,7 +77,7 @@ export default {
return data.project?.ciJobTokenScope?.projects?.nodes ?? [];
},
error() {
- createFlash({ message: this.$options.i18n.projectsFetchError });
+ createAlert({ message: this.$options.i18n.projectsFetchError });
},
},
},
@@ -112,7 +117,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
}
},
async addProject() {
@@ -135,7 +140,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
} finally {
this.clearTargetProjectPath();
this.getProjects();
@@ -161,7 +166,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
- createFlash({ message: error });
+ createAlert({ message: error });
} finally {
this.getProjects();
}
@@ -195,8 +200,8 @@ export default {
</template>
</gl-toggle>
- <div data-testid="token-section">
- <gl-card class="gl-mt-5">
+ <div>
+ <gl-card class="gl-mt-5 gl-mb-3">
<template #header>
<h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5>
</template>
@@ -213,7 +218,16 @@ export default {
<gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
</template>
</gl-card>
-
+ <gl-alert
+ v-if="!jobTokenScopeEnabled"
+ class="gl-mb-3"
+ variant="warning"
+ :dismissible="false"
+ :show-icon="false"
+ data-testid="token-disabled-alert"
+ >
+ {{ $options.i18n.settingDisabledMessage }}
+ </gl-alert>
<token-projects-table :projects="projects" @removeProject="removeProject" />
</div>
</template>
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 94b4ee77e7e..bd425bdc2a8 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -50,6 +50,7 @@ function UsersSelect(currentUser, els, options = {}) {
options.iid = $dropdown.data('iid');
options.issuableType = $dropdown.data('issuableType');
options.targetBranch = $dropdown.data('targetBranch');
+ options.showSuggested = $dropdown.data('showSuggested');
const showNullUser = $dropdown.data('nullUser');
const defaultNullUser = $dropdown.data('nullUserDefault');
const showMenuAbove = $dropdown.data('showMenuAbove');
@@ -340,6 +341,16 @@ function UsersSelect(currentUser, els, options = {}) {
if ($dropdown.hasClass('js-multiselect')) {
const selected = getSelected().filter((i) => i !== 0);
+ if ($dropdown.data('showSuggested')) {
+ const suggested = this.suggestedUsers(users);
+ if (suggested.length) {
+ users = users.filter(
+ (u) => !u.suggested || (u.suggested && selected.indexOf(u.id) !== -1),
+ );
+ users.splice(showDivider + 1, 0, ...suggested);
+ }
+ }
+
if (selected.length > 0) {
if ($dropdown.data('dropdownHeader')) {
showDivider += 1;
@@ -370,6 +381,19 @@ function UsersSelect(currentUser, els, options = {}) {
$dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
},
+ suggestedUsers(users) {
+ const selected = getSelected().filter((i) => i !== 0);
+ const suggestedUsers = users.filter((u) => u.suggested && selected.indexOf(u.id) === -1);
+
+ if (!suggestedUsers.length) return [];
+
+ const items = [
+ { type: 'header', content: $dropdown.data('suggestedReviewersHeader') },
+ ...suggestedUsers,
+ { type: 'header', content: $dropdown.data('allMembersHeader') },
+ ];
+ return items;
+ },
filterable: true,
filterRemote: true,
search: {
@@ -760,6 +784,10 @@ UsersSelect.prototype.users = function (query, options, callback) {
params.approval_rules = true;
}
+ if (isMergeRequest && options.showSuggested) {
+ params.show_suggested = true;
+ }
+
if (isNewMergeRequest) {
params.target_branch = options.targetBranch || null;
}
@@ -791,13 +819,14 @@ UsersSelect.prototype.renderRow = function (
const tooltipAttributes = tooltip
? `data-container="body" data-placement="left" data-title="${tooltip}"`
: '';
+ const dataUserSuggested = user.suggested ? `data-user-suggested=${user.suggested}` : '';
const name =
user?.availability && isUserBusy(user.availability)
? sprintf(__('%{name} (Busy)'), { name: user.name })
: user.name;
return `
- <li data-user-id=${user.id}>
+ <li data-user-id=${user.id} ${dataUserSuggested}>
<a href="#" class="dropdown-menu-user-link gl-display-flex! gl-align-items-center ${linkClasses}" ${tooltipAttributes}>
${this.renderRowAvatar(issuableType, user, img)}
<span class="gl-display-flex gl-flex-direction-column gl-overflow-hidden">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
index 30a0e7c383c..5339d7faf85 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/action_buttons.vue
@@ -74,9 +74,11 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-align-items-flex-start">
<gl-dropdown
v-if="tertiaryButtons.length"
+ v-gl-tooltip
+ :title="__('Options')"
:text="dropdownLabel"
icon="ellipsis_v"
no-caret
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index f782c28ea19..2cfeb7a4bcb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
@@ -139,7 +139,7 @@ export default {
this.fetchingApprovals = false;
})
.catch(() =>
- createFlash({
+ createAlert({
message: FETCH_ERROR,
}),
);
@@ -154,7 +154,7 @@ export default {
this.updateApproval(
() => this.service.approveMergeRequest(),
() =>
- createFlash({
+ createAlert({
message: APPROVE_ERROR,
}),
);
@@ -167,7 +167,7 @@ export default {
this.hasApprovalAuthError = true;
return;
}
- createFlash({
+ createAlert({
message: APPROVE_ERROR,
});
},
@@ -177,7 +177,7 @@ export default {
this.updateApproval(
() => this.service.unapproveMergeRequest(),
() =>
- createFlash({
+ createAlert({
message: UNAPPROVE_ERROR,
}),
);
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
index 7ba387c79b1..d6d1cae4029 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_actions.vue
@@ -1,9 +1,10 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../../event_hub';
import MRWidgetService from '../../services/mr_widget_service';
import {
MANUAL_DEPLOY,
@@ -129,11 +130,12 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: errorMessage,
});
})
.finally(() => {
+ eventHub.$emit('FetchDeployments');
this.actionInProgress = null;
});
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
index 1e363b0f5fb..5efa0e2879e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment/deployment_view_button.vue
@@ -7,7 +7,10 @@ import {
GlLink,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { isSafeURL } from '~/lib/utils/url_utility';
+import { s__, __ } from '~/locale';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import ReviewAppLink from '../review_app_link.vue';
export default {
@@ -19,6 +22,7 @@ export default {
GlIcon,
GlLink,
GlSearchBoxByType,
+ ModalCopyButton,
ReviewAppLink,
},
directives: {
@@ -50,6 +54,13 @@ export default {
filteredChanges() {
return this.deployment?.changes?.filter((change) => change.path.includes(this.searchTerm));
},
+ isSafeUrl() {
+ return isSafeURL(this.deploymentExternalUrl);
+ },
+ },
+ i18n: {
+ copy: __('Copy URL'),
+ copyTitle: s__('Environments|Copy live environment URL'),
},
};
</script>
@@ -57,11 +68,20 @@ export default {
<span class="gl-display-inline-flex">
<gl-button-group v-if="shouldRenderDropdown" size="small">
<review-app-link
+ v-if="isSafeUrl"
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
css-class="deploy-link js-deploy-url inline gl-ml-3"
/>
+ <modal-copy-button
+ v-else
+ :title="$options.i18n.copyTitle"
+ :text="deploymentExternalUrl"
+ size="small"
+ >
+ {{ $options.i18n.copy }}
+ </modal-copy-button>
<gl-dropdown toggle-class="gl-px-2!" size="small" class="js-mr-wigdet-deployment-dropdown">
<template #button-content>
<gl-icon
@@ -90,12 +110,22 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</gl-button-group>
- <review-app-link
- v-else
- :display="appButtonText"
- :link="deploymentExternalUrl"
- size="small"
- css-class="js-deploy-url deploy-link btn btn-default btn-sm inline gl-ml-3"
- />
+ <template v-else>
+ <review-app-link
+ v-if="isSafeUrl"
+ :display="appButtonText"
+ :link="deploymentExternalUrl"
+ size="small"
+ css-class="deploy-link js-deploy-url inline gl-ml-3"
+ />
+ <modal-copy-button
+ v-else
+ :title="$options.i18n.copyTitle"
+ :text="deploymentExternalUrl"
+ size="small"
+ >
+ {{ $options.i18n.copy }}
+ </modal-copy-button>
+ </template>
</span>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
index 300e2a672cb..3d03dbd9db3 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
@@ -315,7 +315,6 @@ export default {
data-qa-selector="mr_widget_extension"
>
<state-container
- :mr="mr"
:status="statusIconName"
:is-loading="isLoadingSummary"
:class="{ 'gl-cursor-pointer': isCollapsible }"
@@ -324,7 +323,7 @@ export default {
@mouseup="onRowMouseUp"
>
<div
- class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
+ class="media-body gl-display-flex gl-flex-direction-row! gl-w-full"
data-testid="widget-extension-top-level"
>
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
index d67ff11f297..e3f87c08ad4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/telemetry.js
@@ -28,7 +28,7 @@ const nonStandardEvents = {
},
counter: {},
},
- testReport: {
+ testSummary: {
uniqueUser: {
expand: ['i_testing_summary_widget_total'],
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 94a1b805b99..870972156c5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -26,16 +26,6 @@ export default {
required: false,
default: true,
},
- divergedCommitsCount: {
- type: Number,
- required: false,
- default: 0,
- },
- targetBranchPath: {
- type: String,
- required: false,
- default: '',
- },
},
computed: {
closesText() {
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 822c5a68093..932659f3c89 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
@@ -16,7 +16,8 @@ export default {
props: {
mr: {
type: Object,
- required: true,
+ required: false,
+ default: null,
},
isLoading: {
type: Boolean,
@@ -80,6 +81,7 @@ export default {
</div>
</div>
<div
+ v-if="mr"
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
>
<gl-button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index e2a9caf5419..2b22033514f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -1,6 +1,7 @@
<script>
import { s__ } from '~/locale';
import StatusIcon from '../mr_widget_status_icon.vue';
+import { DETAILED_MERGE_STATUS } from '../../constants';
export default {
i18n: {
@@ -22,7 +23,7 @@ export default {
failedText() {
if (this.mr.approvals && !this.mr.isApproved) {
return this.$options.i18n.approvalNeeded;
- } else if (this.mr.blockingMergeRequests?.total_count > 0) {
+ } else if (this.mr.detailedMergeStatus === DETAILED_MERGE_STATUS.BLOCKED_STATUS) {
return this.$options.i18n.blockingMergeRequests;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 3c6c2a44e70..92a7fa39cdc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -2,7 +2,7 @@
import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
@@ -113,7 +113,7 @@ export default {
})
.catch(() => {
this.isCancellingAutoMerge = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
@@ -141,7 +141,7 @@ export default {
})
.catch(() => {
this.isRemovingSourceBranch = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index e9298b0c856..46392565088 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import modalEventHub from '~/projects/commit/event_hub';
@@ -131,7 +131,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
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 37c8d5d15f3..f6843c1f3d3 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
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
@@ -111,7 +111,7 @@ export default {
if (error.response && error.response.data && error.response.data.merge_error) {
this.rebasingError = error.response.data.merge_error;
} else {
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
}
@@ -142,7 +142,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
stopPolling();
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index 3cbd171a035..853895a4296 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -11,6 +11,12 @@ export default {
GlSprintf,
StatusIcon,
},
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
computed: {
troubleshootingDocsPath() {
return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
@@ -29,7 +35,14 @@ export default {
<status-icon status="failed" />
<div class="media-body space-children">
<span class="gl-font-weight-bold">
- <gl-sprintf :message="$options.i18n.failedMessage">
+ <span v-if="mr.isPipelineBlocked">
+ {{
+ s__(
+ `mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`,
+ )
+ }}
+ </span>
+ <gl-sprintf v-else :message="$options.i18n.failedMessage">
<template #link="{ content }">
<gl-link :href="troubleshootingDocsPath" target="_blank">
{{ content }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 78430abcfe9..1298c1316e2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -14,10 +14,10 @@ import {
import { isEmpty } from 'lodash';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import simplePoll from '~/lib/utils/simple_poll';
-import { __, s__ } from '~/locale';
+import { __, s__, n__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -325,15 +325,20 @@ export default {
);
},
sourceBranchDeletedText() {
- if (this.removeSourceBranch) {
- return this.mr.state === 'merged'
- ? __('Deleted the source branch.')
- : __('Source branch will be deleted.');
+ const isPreMerge = this.mr.state !== 'merged';
+
+ if (isPreMerge) {
+ return this.mr.shouldRemoveSourceBranch
+ ? __('Source branch will be deleted.')
+ : __('Source branch will not be deleted.');
}
- return this.mr.state === 'merged'
- ? __('Did not delete the source branch.')
- : __('Source branch will not be deleted.');
+ return this.mr.sourceBranchRemoved
+ ? __('Deleted the source branch.')
+ : __('Did not delete the source branch.');
+ },
+ sourceHasDivergedFromTarget() {
+ return this.mr.divergedCommitsCount > 0;
},
showMergeDetailsHeader() {
return ['readyToMerge'].indexOf(this.mr.state) >= 0;
@@ -439,7 +444,7 @@ export default {
.catch(() => {
this.isMakingRequest = false;
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
@@ -483,7 +488,7 @@ export default {
}
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting the source branch. Please try again.'),
});
});
@@ -507,6 +512,8 @@ export default {
mergeAndSquashCommitTemplatesHintText: s__(
'mrWidget|To change these default messages, edit the templates for both the merge and squash commit messages. %{linkStart}Learn more.%{linkEnd}',
),
+ sourceDivergedFromTargetText: s__('mrWidget|The source branch is %{link} the target branch'),
+ divergedCommits: (count) => n__('%d commit behind', '%d commits behind', count),
},
};
</script>
@@ -530,130 +537,148 @@ export default {
<div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1">
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
- <gl-button-group v-if="shouldShowMergeControls" class="gl-align-self-start">
- <gl-button
- size="medium"
- category="primary"
- class="accept-merge-request"
- data-testid="merge-button"
- variant="confirm"
- :disabled="isMergeButtonDisabled"
- :loading="isMakingRequest"
- data-qa-selector="merge_button"
- @click="handleMergeButtonClick(isAutoMergeAvailable)"
- >{{ mergeButtonText }}</gl-button
- >
- <gl-dropdown
- v-if="shouldShowMergeImmediatelyDropdown"
- v-gl-tooltip.hover.focus="__('Select merge moment')"
- :disabled="isMergeButtonDisabled"
- variant="confirm"
- data-qa-selector="merge_moment_dropdown"
- toggle-class="btn-icon js-merge-moment"
- >
- <template #button-content>
- <gl-icon name="chevron-down" class="mr-0" />
- <span class="sr-only">{{ __('Select merge moment') }}</span>
- </template>
- <gl-dropdown-item
- icon-name="warning"
- button-class="accept-merge-request js-merge-immediately-button"
- data-qa-selector="merge_immediately_menu_item"
- @click="handleMergeImmediatelyButtonClick"
+ <template v-if="shouldShowMergeControls">
+ <div class="gl-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-mb-5">
+ <gl-form-checkbox
+ v-if="canRemoveSourceBranch"
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5"
>
- {{ __('Merge immediately') }}
- </gl-dropdown-item>
- <merge-immediately-confirmation-dialog
- ref="confirmationDialog"
- :docs-url="mr.mergeImmediatelyDocsPath"
- @mergeImmediately="onMergeImmediatelyConfirmation"
+ {{ __('Delete source branch') }}
+ </gl-form-checkbox>
+
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ v-model="squashBeforeMerge"
+ :help-path="mr.squashBeforeMergeHelpPath"
+ :is-disabled="isSquashReadOnly"
+ class="gl-mr-5"
/>
- </gl-dropdown>
- <merge-train-failed-pipeline-confirmation-dialog
- :visible="isPipelineFailedModalVisibleMergeTrain"
- @startMergeTrain="onStartMergeTrainConfirmation"
- @cancel="isPipelineFailedModalVisibleMergeTrain = false"
- />
- <merge-failed-pipeline-confirmation-dialog
- :visible="isPipelineFailedModalVisibleNormalMerge"
- @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
- @cancel="isPipelineFailedModalVisibleNormalMerge = false"
- />
- </gl-button-group>
- <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" />
- <div
- v-if="shouldShowMergeControls"
- class="gl-display-flex gl-align-items-center gl-flex-wrap gl-w-full gl-order-n1 gl-mb-5"
- >
- <gl-form-checkbox
- v-if="canRemoveSourceBranch"
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- class="js-remove-source-branch-checkbox gl-display-flex gl-align-items-center gl-mr-5"
- >
- {{ __('Delete source branch') }}
- </gl-form-checkbox>
-
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- v-model="squashBeforeMerge"
- :help-path="mr.squashBeforeMergeHelpPath"
- :is-disabled="isSquashReadOnly"
- class="gl-mr-5"
- />
-
- <gl-form-checkbox
- v-if="shouldShowSquashEdit || shouldShowMergeEdit"
- v-model="editCommitMessage"
- data-testid="widget_edit_commit_message"
- class="gl-display-flex gl-align-items-center"
- >
- {{ __('Edit commit message') }}
- </gl-form-checkbox>
- </div>
- <div
- v-if="editCommitMessage"
- class="gl-w-full gl-order-n1"
- data-testid="edit_commit_message"
- >
- <ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4">
- <commit-edit
- v-if="shouldShowSquashEdit"
- :value="squashCommitMessage"
- :label="__('Squash commit message')"
- input-id="squash-message-edit"
- class="gl-m-0! gl-p-0!"
- @input="setSquashCommitMessage"
+
+ <gl-form-checkbox
+ v-if="shouldShowSquashEdit || shouldShowMergeEdit"
+ v-model="editCommitMessage"
+ data-testid="widget_edit_commit_message"
+ class="gl-display-flex gl-align-items-center"
>
- <template #header>
- <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
+ {{ __('Edit commit message') }}
+ </gl-form-checkbox>
+ </div>
+ <div class="gl-w-full gl-text-gray-500 gl-mb-5">
+ <added-commit-message
+ :is-squash-enabled="squashBeforeMerge"
+ :is-fast-forward-enabled="!shouldShowMergeEdit"
+ :commits-count="commitsCount"
+ :target-branch="stateData.targetBranch"
+ />
+ <template v-if="mr.relatedLinks">
+ &middot;
+ <related-links
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ :show-assign-to-me="false"
+ :diverged-commits-count="mr.divergedCommitsCount"
+ :target-branch-path="mr.targetBranchPath"
+ class="mr-ready-merge-related-links gl-display-inline"
+ />
+ </template>
+ </div>
+ <div v-if="editCommitMessage" class="gl-w-full" data-testid="edit_commit_message">
+ <ul class="border-top commits-list flex-list gl-list-style-none gl-p-0 gl-pt-4">
+ <commit-edit
+ v-if="shouldShowSquashEdit"
+ :value="squashCommitMessage"
+ :label="__('Squash commit message')"
+ input-id="squash-message-edit"
+ class="gl-m-0! gl-p-0!"
+ @input="setSquashCommitMessage"
+ >
+ <template #header>
+ <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
+ </template>
+ </commit-edit>
+ <commit-edit
+ v-if="shouldShowMergeEdit"
+ :value="commitMessage"
+ :label="__('Merge commit message')"
+ input-id="merge-message-edit"
+ class="gl-m-0! gl-p-0!"
+ @input="setCommitMessage"
+ />
+ <li class="gl-m-0! gl-p-0!">
+ <p class="form-text text-muted">
+ <gl-sprintf :message="commitTemplateHintText">
+ <template #link="{ content }">
+ <gl-link
+ :href="commitTemplateHelpPage"
+ class="inline-link"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </li>
+ </ul>
+ </div>
+ <gl-button-group class="gl-align-self-start">
+ <gl-button
+ size="medium"
+ category="primary"
+ class="accept-merge-request"
+ data-testid="merge-button"
+ variant="confirm"
+ :disabled="isMergeButtonDisabled"
+ :loading="isMakingRequest"
+ data-qa-selector="merge_button"
+ @click="handleMergeButtonClick(isAutoMergeAvailable)"
+ >{{ mergeButtonText }}</gl-button
+ >
+ <gl-dropdown
+ v-if="shouldShowMergeImmediatelyDropdown"
+ v-gl-tooltip.hover.focus="__('Select merge moment')"
+ :disabled="isMergeButtonDisabled"
+ variant="confirm"
+ data-qa-selector="merge_moment_dropdown"
+ toggle-class="btn-icon js-merge-moment"
+ >
+ <template #button-content>
+ <gl-icon name="chevron-down" class="mr-0" />
+ <span class="sr-only">{{ __('Select merge moment') }}</span>
</template>
- </commit-edit>
- <commit-edit
- v-if="shouldShowMergeEdit"
- :value="commitMessage"
- :label="__('Merge commit message')"
- input-id="merge-message-edit"
- class="gl-m-0! gl-p-0!"
- @input="setCommitMessage"
+ <gl-dropdown-item
+ icon-name="warning"
+ button-class="accept-merge-request js-merge-immediately-button"
+ data-qa-selector="merge_immediately_menu_item"
+ @click="handleMergeImmediatelyButtonClick"
+ >
+ {{ __('Merge immediately') }}
+ </gl-dropdown-item>
+ <merge-immediately-confirmation-dialog
+ ref="confirmationDialog"
+ :docs-url="mr.mergeImmediatelyDocsPath"
+ @mergeImmediately="onMergeImmediatelyConfirmation"
+ />
+ </gl-dropdown>
+ <merge-train-failed-pipeline-confirmation-dialog
+ :visible="isPipelineFailedModalVisibleMergeTrain"
+ @startMergeTrain="onStartMergeTrainConfirmation"
+ @cancel="isPipelineFailedModalVisibleMergeTrain = false"
/>
- <li class="gl-m-0! gl-p-0!">
- <p class="form-text text-muted">
- <gl-sprintf :message="commitTemplateHintText">
- <template #link="{ content }">
- <gl-link :href="commitTemplateHelpPage" class="inline-link" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </li>
- </ul>
- </div>
+ <merge-failed-pipeline-confirmation-dialog
+ :visible="isPipelineFailedModalVisibleNormalMerge"
+ @mergeWithFailedPipeline="onMergeWithFailedPipelineConfirmation"
+ @cancel="isPipelineFailedModalVisibleNormalMerge = false"
+ />
+ </gl-button-group>
+ <merge-train-helper-icon v-if="shouldRenderMergeTrainHelperIcon" class="gl-mx-3" />
+ </template>
<div
- v-if="!shouldShowMergeControls"
+ v-else
class="gl-w-full gl-order-n1 mr-widget-merge-details"
data-qa-selector="merged_status_content"
>
@@ -661,13 +686,11 @@ export default {
{{ __('Merge details') }}
</p>
<ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600">
- <li v-if="mr.divergedCommitsCount > 0" class="gl-line-height-normal">
- <gl-sprintf
- :message="s__('mrWidget|The source branch is %{link} the target branch')"
- >
+ <li v-if="sourceHasDivergedFromTarget" class="gl-line-height-normal">
+ <gl-sprintf :message="$options.i18n.sourceDivergedFromTargetText">
<template #link>
<gl-link :href="mr.targetBranchPath">{{
- n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount)
+ $options.i18n.divergedCommits(mr.divergedCommitsCount)
}}</gl-link>
</template>
</gl-sprintf>
@@ -696,29 +719,6 @@ export default {
</li>
</ul>
</div>
- <div
- v-else
- :class="{ 'gl-mb-5': shouldShowMergeControls }"
- class="gl-w-full gl-order-n1 gl-text-gray-500"
- >
- <added-commit-message
- :is-squash-enabled="squashBeforeMerge"
- :is-fast-forward-enabled="!shouldShowMergeEdit"
- :commits-count="commitsCount"
- :target-branch="stateData.targetBranch"
- />
- <template v-if="mr.relatedLinks">
- &middot;
- <related-links
- :state="mr.state"
- :related-links="mr.relatedLinks"
- :show-assign-to-me="false"
- :diverged-commits-count="mr.divergedCommitsCount"
- :target-branch-path="mr.targetBranchPath"
- class="mr-ready-merge-related-links gl-display-inline"
- />
- </template>
- </div>
</div>
</div>
</div>
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 0458e9dfaf5..dee27a5d5b5 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
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { produce } from 'immer';
import $ from 'jquery';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { __ } from '~/locale';
import MergeRequest from '~/merge_request';
@@ -77,7 +77,7 @@ export default {
},
) {
if (errors?.length) {
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
@@ -130,7 +130,7 @@ export default {
},
)
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
}),
)
@@ -152,7 +152,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
deleted file mode 100644
index 18fdb29ba54..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue
+++ /dev/null
@@ -1,137 +0,0 @@
-<script>
-import { GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import Poll from '~/lib/utils/poll';
-import { n__ } from '~/locale';
-import MrWidgetExpanableSection from '../mr_widget_expandable_section.vue';
-import TerraformPlan from './terraform_plan.vue';
-
-export default {
- name: 'MRWidgetTerraformContainer',
- components: {
- GlSkeletonLoader,
- GlSprintf,
- MrWidgetExpanableSection,
- TerraformPlan,
- },
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- loading: true,
- plansObject: {},
- poll: null,
- };
- },
- computed: {
- inValidPlanCountText() {
- if (this.numberOfInvalidPlans === 0) {
- return null;
- }
-
- return n__(
- 'Terraform|%{number} Terraform report failed to generate',
- 'Terraform|%{number} Terraform reports failed to generate',
- this.numberOfInvalidPlans,
- );
- },
- numberOfInvalidPlans() {
- return Object.values(this.plansObject).filter((plan) => plan.tf_report_error).length;
- },
- numberOfPlans() {
- return Object.keys(this.plansObject).length;
- },
- numberOfValidPlans() {
- return this.numberOfPlans - this.numberOfInvalidPlans;
- },
- validPlanCountText() {
- if (this.numberOfValidPlans === 0) {
- return null;
- }
-
- return n__(
- 'Terraform|%{number} Terraform report was generated in your pipelines',
- 'Terraform|%{number} Terraform reports were generated in your pipelines',
- this.numberOfValidPlans,
- );
- },
- },
- created() {
- this.fetchPlans();
- },
- beforeDestroy() {
- this.poll.stop();
- },
- methods: {
- fetchPlans() {
- this.loading = true;
-
- this.poll = new Poll({
- resource: {
- fetchPlans: () => axios.get(this.endpoint),
- },
- data: this.endpoint,
- method: 'fetchPlans',
- successCallback: ({ data }) => {
- this.plansObject = data;
-
- if (this.numberOfPlans > 0) {
- this.loading = false;
- this.poll.stop();
- }
- },
- errorCallback: () => {
- this.plansObject = { bad_plan: { tf_report_error: 'api_error' } };
- this.loading = false;
- this.poll.stop();
- },
- });
-
- this.poll.makeRequest();
- },
- },
-};
-</script>
-
-<template>
- <section class="mr-widget-section">
- <div v-if="loading" class="mr-widget-body">
- <gl-skeleton-loader />
- </div>
-
- <mr-widget-expanable-section v-else>
- <template #header>
- <div
- data-testid="terraform-header-text"
- class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column"
- >
- <p v-if="validPlanCountText" class="gl-m-0">
- <gl-sprintf :message="validPlanCountText">
- <template #number>
- <strong>{{ numberOfValidPlans }}</strong>
- </template>
- </gl-sprintf>
- </p>
-
- <p v-if="inValidPlanCountText" class="gl-m-0">
- <gl-sprintf :message="inValidPlanCountText">
- <template #number>
- <strong>{{ numberOfInvalidPlans }}</strong>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </template>
-
- <template #content>
- <div class="mr-widget-body gl-pb-1">
- <terraform-plan v-for="(plan, key) in plansObject" :key="key" :plan="plan" />
- </div>
- </template>
- </mr-widget-expanable-section>
- </section>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue b/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
deleted file mode 100644
index 1e5f7361966..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue
+++ /dev/null
@@ -1,119 +0,0 @@
-<script>
-import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export default {
- name: 'TerraformPlan',
- components: {
- GlIcon,
- GlLink,
- GlSprintf,
- },
- props: {
- plan: {
- required: true,
- type: Object,
- },
- },
- i18n: {
- changes: s__(
- 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
- ),
- generationErrored: s__('Terraform|Generating the report caused an error.'),
- namedReportFailed: s__('Terraform|The job %{name} failed to generate a report.'),
- namedReportGenerated: s__('Terraform|The job %{name} generated a report.'),
- reportFailed: s__('Terraform|A report failed to generate.'),
- reportGenerated: s__('Terraform|A report was generated in your pipelines.'),
- },
- computed: {
- addNum() {
- return Number(this.plan.create);
- },
- changeNum() {
- return Number(this.plan.update);
- },
- deleteNum() {
- return Number(this.plan.delete);
- },
- iconType() {
- return this.validPlanValues ? 'doc-changes' : 'warning';
- },
- reportChangeText() {
- if (this.validPlanValues) {
- return this.$options.i18n.changes;
- }
-
- return this.$options.i18n.generationErrored;
- },
- reportHeaderText() {
- if (this.validPlanValues) {
- return this.plan.job_name
- ? this.$options.i18n.namedReportGenerated
- : this.$options.i18n.reportGenerated;
- }
-
- return this.plan.job_name
- ? this.$options.i18n.namedReportFailed
- : this.$options.i18n.reportFailed;
- },
- validPlanValues() {
- return this.addNum + this.changeNum + this.deleteNum >= 0;
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-pb-3">
- <span
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-px-2"
- >
- <gl-icon :name="iconType" :size="16" data-testid="change-type-icon" />
- </span>
-
- <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column flex-md-row gl-pl-3">
- <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-pr-3">
- <p class="gl-mb-3 gl-line-height-normal">
- <gl-sprintf :message="reportHeaderText">
- <template #name>
- <strong>{{ plan.job_name }}</strong>
- </template>
- </gl-sprintf>
- </p>
-
- <p class="gl-mb-3 gl-line-height-normal">
- <gl-sprintf :message="reportChangeText">
- <template #addNum>
- <strong>{{ addNum }}</strong>
- </template>
-
- <template #changeNum>
- <strong>{{ changeNum }}</strong>
- </template>
-
- <template #deleteNum>
- <strong>{{ deleteNum }}</strong>
- </template>
- </gl-sprintf>
- </p>
- </div>
-
- <div>
- <gl-link
- v-if="plan.job_path"
- :href="plan.job_path"
- target="_blank"
- data-testid="terraform-report-link"
- data-track-action="click_terraform_mr_plan_button"
- data-track-label="mr_widget_terraform_mr_plan_button"
- data-track-property="terraform_mr_plan_button"
- class="btn btn-sm"
- rel="noopener"
- >
- {{ __('View full log') }}
- <gl-icon name="external-link" />
- </gl-link>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
new file mode 100644
index 00000000000..d1ade2886f4
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -0,0 +1,98 @@
+<script>
+import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import Actions from '../action_buttons.vue';
+import { generateText } from '../extensions/utils';
+import ContentRow from './widget_content_row.vue';
+
+export default {
+ name: 'DynamicContent',
+ components: {
+ GlBadge,
+ GlLink,
+ Actions,
+ ContentRow,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ widgetName: {
+ type: String,
+ required: true,
+ },
+ level: {
+ type: Number,
+ required: false,
+ default: 2,
+ },
+ },
+ computed: {
+ statusIcon() {
+ return this.data.icon?.name || undefined;
+ },
+ generatedText() {
+ return generateText(this.data.text);
+ },
+ generatedSubtext() {
+ return generateText(this.data.subtext);
+ },
+ generatedSupportingText() {
+ return generateText(this.data.supportingText);
+ },
+ },
+ methods: {
+ onClickedAction(action) {
+ this.$emit('clickedAction', action);
+ },
+ },
+};
+</script>
+
+<template>
+ <content-row
+ :level="level"
+ :status-icon-name="statusIcon"
+ :widget-name="widgetName"
+ :header="data.header"
+ >
+ <template #body>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div>
+ <p v-safe-html="generatedText" class="gl-mb-0"></p>
+ <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link>
+ <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p>
+ <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
+ {{ data.badge.text }}
+ </gl-badge>
+ <actions
+ :widget="widgetName"
+ :tertiary-buttons="data.actions"
+ class="gl-ml-auto gl-pl-3"
+ @clickedAction="onClickedAction"
+ />
+ <p v-if="data.subtext" v-safe-html="generatedSubtext" class="gl-m-0 gl-font-sm"></p>
+ </div>
+ <ul
+ v-if="data.children && data.children.length > 0 && level === 2"
+ class="gl-m-0 gl-p-0 gl-list-style-none"
+ >
+ <li>
+ <dynamic-content
+ v-for="(childData, index) in data.children"
+ :key="childData.id || index"
+ :data="childData"
+ :widget-name="widgetName"
+ :level="3"
+ data-qa-selector="child_content"
+ @clickedAction="onClickedAction"
+ />
+ </li>
+ </ul>
+ </div>
+ </template>
+ </content-row>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
new file mode 100644
index 00000000000..ff17de343d6
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { EXTENSION_ICON_CLASS, EXTENSION_ICON_NAMES } from '../../constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlIcon,
+ },
+ props: {
+ level: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ iconName: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ iconAriaLabel() {
+ return `${capitalizeFirstCharacter(this.iconName)} ${this.name}`;
+ },
+ iconSize() {
+ return this.level === 1 ? 16 : 12;
+ },
+ },
+ EXTENSION_ICON_NAMES,
+ EXTENSION_ICON_CLASS,
+};
+</script>
+
+<template>
+ <div :class="[$options.EXTENSION_ICON_CLASS[iconName]]" class="gl-mr-3">
+ <gl-loading-icon v-if="isLoading" size="md" inline />
+ <div
+ v-else
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-rounded-full gl-bg-gray-10"
+ :class="{
+ 'gl-p-2': level === 1,
+ }"
+ >
+ <div class="gl-rounded-full gl-bg-white">
+ <gl-icon
+ :name="$options.EXTENSION_ICON_NAMES[iconName]"
+ :size="iconSize"
+ :aria-label="iconAriaLabel"
+ :data-qa-selector="`status_${iconName}_icon`"
+ class="gl-display-block"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index c9fc2dde0bd..94359d7d6ac 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
@@ -4,10 +4,11 @@ import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
-import StatusIcon from '../extensions/status_icon.vue';
import ActionButtons from '../action_buttons.vue';
import { EXTENSION_ICONS } from '../../constants';
-import ContentSection from './widget_content_section.vue';
+import ContentRow from './widget_content_row.vue';
+import DynamicContent from './dynamic_content.vue';
+import StatusIcon from './status_icon.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
@@ -18,7 +19,8 @@ export default {
StatusIcon,
GlButton,
GlLoadingIcon,
- ContentSection,
+ ContentRow,
+ DynamicContent,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,7 +61,7 @@ export default {
},
// If the content slot is not used, this value will be used as a fallback.
content: {
- type: Object,
+ type: Array,
required: false,
default: undefined,
},
@@ -187,7 +189,7 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
- <div class="media gl-p-5">
+ <div class="gl-p-5 gl-align-items-center gl-display-flex">
<status-icon
:level="1"
:name="widgetName"
@@ -227,23 +229,34 @@ export default {
</div>
<div
v-if="!isCollapsed || contentError"
- class="mr-widget-grouped-section gl-relative"
+ class="gl-relative gl-bg-gray-10"
data-testid="widget-extension-collapsed-section"
>
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
- <gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
+ <gl-loading-icon size="sm" inline /> {{ loadingText }}
+ </div>
+ <div v-else class="gl-px-5 gl-display-flex">
+ <content-row
+ v-if="contentError"
+ :level="2"
+ :status-icon-name="$options.failedStatusIcon"
+ :widget-name="widgetName"
+ >
+ <template #body>
+ {{ contentError }}
+ </template>
+ </content-row>
+ <div v-else class="gl-w-full">
+ <slot name="content">
+ <dynamic-content
+ v-for="(data, index) in content"
+ :key="data.id || index"
+ :data="data"
+ :widget-name="widgetName"
+ />
+ </slot>
+ </div>
</div>
- <content-section
- v-else-if="contentError"
- class="report-block-container"
- :status-icon-name="$options.failedStatusIcon"
- :widget-name="widgetName"
- >
- {{ contentError }}
- </content-section>
- <slot v-else name="content">
- {{ content }}
- </slot>
</div>
</section>
</template>
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
new file mode 100644
index 00000000000..ee81f0950a8
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlSafeHtmlDirective } from '@gitlab/ui';
+import { EXTENSION_ICONS } from '../../constants';
+import { generateText } from '../extensions/utils';
+import StatusIcon from './status_icon.vue';
+
+export default {
+ components: {
+ StatusIcon,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
+ props: {
+ level: {
+ type: Number,
+ required: true,
+ validator: (value) => value === 2 || value === 3,
+ },
+ statusIconName: {
+ type: String,
+ default: '',
+ required: false,
+ validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
+ },
+ widgetName: {
+ type: String,
+ required: true,
+ },
+ header: {
+ type: [String, Array],
+ default: '',
+ required: false,
+ },
+ },
+ computed: {
+ generatedHeader() {
+ return generateText(Array.isArray(this.header) ? this.header[0] : this.header);
+ },
+ generatedSubheader() {
+ return Array.isArray(this.header) && this.header[1] ? generateText(this.header[1]) : '';
+ },
+ },
+};
+</script>
+<template>
+ <div
+ class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline"
+ :class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
+ >
+ <status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" />
+ <div>
+ <slot name="header">
+ <div v-if="header" class="gl-mb-2">
+ <strong v-safe-html="generatedHeader" class="gl-display-block"></strong
+ ><span
+ v-if="generatedSubheader"
+ v-safe-html="generatedSubheader"
+ class="gl-display-block"
+ ></span>
+ </div>
+ </slot>
+ <div class="gl-display-flex gl-align-items-baseline gl-w-full">
+ <slot name="body"></slot>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
deleted file mode 100644
index 61e3744b5dc..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
-import { EXTENSION_ICONS } from '../../constants';
-import StatusIcon from '../extensions/status_icon.vue';
-
-export default {
- components: {
- StatusIcon,
- },
- props: {
- statusIconName: {
- type: String,
- default: '',
- required: false,
- validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value),
- },
- widgetName: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <div class="gl-px-7">
- <div class="gl-pl-4 gl-display-flex">
- <status-icon
- v-if="statusIconName"
- :level="2"
- :name="widgetName"
- :icon-name="statusIconName"
- />
- <slot name="default"></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index be4e34ffff0..c6baf3b46ff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -180,3 +180,16 @@ export const INVALID_RULES_DOCS_PATH = helpPagePath(
anchor: 'invalid-rules',
},
);
+
+export const DETAILED_MERGE_STATUS = {
+ MERGEABLE: 'MERGEABLE',
+ CHECKING: 'CHECKING',
+ NOT_OPEN: 'NOT_OPEN',
+ DISCUSSIONS_NOT_RESOLVED: 'DISCUSSIONS_NOT_RESOLVED',
+ NOT_APPROVED: 'NOT_APPROVED',
+ DRAFT_STATUS: 'DRAFT_STATUS',
+ BLOCKED_STATUS: 'BLOCKED_STATUS',
+ POLICIES_DENIED: 'POLICIES_DENIED',
+ CI_MUST_PASS: 'CI_MUST_PASS',
+ CI_STILL_RUNNING: 'CI_STILL_RUNNING',
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index c8a2a8d119b..a3f70b551bf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -6,7 +6,7 @@ import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/ap
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import { stateToComponentMap as classState } from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
@@ -86,9 +86,6 @@ export default {
import('../reports/codequality_report/grouped_codequality_reports_app.vue'),
GroupedTestReportsApp: () =>
import('../reports/grouped_test_report/grouped_test_reports_app.vue'),
- TerraformPlan: () => import('./components/terraform/mr_widget_terraform_container.vue'),
- GroupedAccessibilityReportsApp: () =>
- import('../reports/accessibility_report/grouped_accessibility_reports_app.vue'),
MrWidgetApprovals,
SecurityReportsApp: () => import('~/vue_shared/security_reports/security_reports_app.vue'),
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
@@ -218,12 +215,6 @@ export default {
hasAlerts() {
return this.hasMergeError || this.showMergePipelineForkWarning;
},
- shouldShowExtension() {
- return (
- window.gon?.features?.refactorMrWidgetsExtensions ||
- window.gon?.features?.refactorMrWidgetsExtensionsUser
- );
- },
shouldShowSecurityExtension() {
return window.gon?.features?.refactorSecurityExtension;
},
@@ -276,7 +267,7 @@ export default {
this.initWidget(data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Unable to load the merge request widget. Try reloading the page.'),
}),
);
@@ -368,7 +359,7 @@ export default {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
}),
);
@@ -427,7 +418,7 @@ export default {
.catch(() => this.throwDeploymentsError());
},
throwDeploymentsError() {
- createFlash({
+ createAlert({
message: __(
'Something went wrong while fetching the environments for this merge request. Please try again.',
),
@@ -447,7 +438,7 @@ export default {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Something went wrong. Please try again.'),
}),
);
@@ -506,17 +497,24 @@ export default {
eventHub.$on('DisablePolling', () => {
this.stopPolling();
});
+
+ eventHub.$on('FetchDeployments', () => {
+ this.fetchPreMergeDeployments();
+ if (this.shouldRenderMergedPipeline) {
+ this.fetchPostMergeDeployments();
+ }
+ });
},
dismissSuggestPipelines() {
this.mr.isDismissedSuggestPipeline = true;
},
registerTerraformPlans() {
- if (this.shouldRenderTerraformPlans && this.shouldShowExtension) {
+ if (this.shouldRenderTerraformPlans) {
registerExtension(terraformExtension);
}
},
registerAccessibilityExtension() {
- if (this.shouldShowAccessibilityReport && this.shouldShowExtension) {
+ if (this.shouldShowAccessibilityReport) {
registerExtension(accessibilityExtension);
}
},
@@ -620,16 +618,6 @@ export default {
:pipeline-path="mr.pipeline.path"
/>
- <terraform-plan
- v-if="mr.terraformReportsPath && !shouldShowExtension"
- :endpoint="mr.terraformReportsPath"
- />
-
- <grouped-accessibility-reports-app
- v-if="shouldShowAccessibilityReport && !shouldShowExtension"
- :endpoint="mr.accessibilityReportPath"
- />
-
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
index eac72ffb2f2..516ba104d7b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.query.graphql
@@ -10,6 +10,7 @@ query getState($projectPath: ID!, $iid: String!) {
availableAutoMergeStrategies
commitCount
conflicts
+ detailedMergeStatus
diffHeadSha
mergeError
mergeStatus
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 7a458f9ce7e..81cb20475cc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -1,3 +1,4 @@
+import { DETAILED_MERGE_STATUS } from '../constants';
import { stateKey } from './state_maps';
export default function deviseState() {
@@ -7,7 +8,7 @@ export default function deviseState() {
return stateKey.archived;
} else if (this.branchMissing) {
return stateKey.missingBranch;
- } else if (this.mergeStatus === 'unchecked' || this.mergeStatus === 'checking') {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
return stateKey.checking;
} else if (this.hasConflicts) {
return stateKey.conflicts;
@@ -15,19 +16,20 @@ export default function deviseState() {
return stateKey.rebase;
} else if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
return stateKey.mergeChecksFailed;
- } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
return stateKey.pipelineFailed;
- } else if (this.draft) {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
return stateKey.draft;
- } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
+ } else if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
return stateKey.unresolvedDiscussions;
- } else if (this.isPipelineBlocked) {
- return stateKey.pipelineBlocked;
} else if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
} else if (this.autoMergeEnabled && !this.mergeError) {
return stateKey.autoMergeEnabled;
- } else if (this.canBeMerged) {
+ } else if (
+ this.detailedMergeStatus === DETAILED_MERGE_STATUS.MERGEABLE ||
+ this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_STILL_RUNNING
+ ) {
return stateKey.readyToMerge;
}
return null;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index e6ff586892f..731d3886f61 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -219,6 +219,7 @@ export default class MergeRequestStore {
this.shouldBeRebased = mergeRequest.shouldBeRebased;
this.draft = mergeRequest.draft;
this.mergeRequestState = mergeRequest.state;
+ this.detailedMergeStatus = mergeRequest.detailedMergeStatus;
this.setState();
}
@@ -291,6 +292,7 @@ export default class MergeRequestStore {
this.suggestPipelineFeatureId = data.suggest_pipeline_feature_id;
this.isDismissedSuggestPipeline = data.is_dismissed_suggest_pipeline;
this.securityReportsDocsPath = data.security_reports_docs_path;
+ this.securityConfigurationPath = data.security_configuration_path;
// code quality
const blobPath = data.blob_path || {};
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 84bd6bca601..c93057c491c 100644
--- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue
@@ -1,6 +1,5 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import CiIcon from './ci_icon.vue';
/**
* Renders CI Badge link with CI icon and status text based on
@@ -27,6 +26,7 @@ import CiIcon from './ci_icon.vue';
export default {
components: {
+ GlLink,
CiIcon,
},
directives: {
@@ -61,29 +61,21 @@ export default {
return className ? `ci-status ci-${className}` : 'ci-status';
},
},
- methods: {
- navigateToPipeline() {
- visitUrl(this.detailsPath);
-
- // event used for tracking
- this.$emit('ciStatusBadgeClick');
- },
- },
};
</script>
<template>
- <a
+ <gl-link
v-gl-tooltip
:class="cssClass"
- class="gl-cursor-pointer"
:title="title"
data-qa-selector="status_badge_link"
- @click="navigateToPipeline"
+ :href="detailsPath"
+ @click="$emit('ciStatusBadgeClick')"
>
<ci-icon :status="status" :css-classes="iconClasses" />
<template v-if="showText">
{{ status.text }}
</template>
- </a>
+ </gl-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
index a88a4ca5cb8..75386a3cd01 100644
--- a/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/color_select_dropdown/color_select_root.vue
@@ -1,6 +1,6 @@
<script>
import { isString } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { DEFAULT_COLOR, COLOR_WIDGET_COLOR, DROPDOWN_VARIANT, ISSUABLE_COLORS } from './constants';
@@ -97,7 +97,7 @@ export default {
return DEFAULT_COLOR;
},
error() {
- createFlash({
+ createAlert({
message: this.$options.i18n.fetchingError,
captureError: true,
});
@@ -161,7 +161,7 @@ export default {
});
})
.catch((error) =>
- createFlash({
+ createAlert({
message: this.$options.i18n.updatingError,
captureError: true,
error,
diff --git a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
index 298c7bc50cc..31c98d1e3a7 100644
--- a/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/confidentiality_badge.vue
@@ -33,7 +33,7 @@ export default {
:title="confidentialTooltip"
icon="eye-slash"
variant="warning"
- class="gl-display-inline gl-mr-2"
+ class="gl-display-inline gl-mr-3"
>{{ __('Confidential') }}</gl-badge
>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index ffe09634a3b..4873996d357 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -56,3 +56,9 @@ export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
export const TOKEN_TITLE_RELEASE = __('Release');
export const TOKEN_TITLE_TYPE = __('Type');
+
+// As health status gets reused between issue lists and boards
+// this is in the shared constants. Until we have not decoupled the EE filtered search bar
+// from the CE component, we need to keep this in the CE code.
+// https://gitlab.com/gitlab-org/gitlab/-/issues/377838
+export const TOKEN_TYPE_HEALTH = 'health_status';
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index e311df6e66f..8821084ef35 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -12,7 +12,7 @@ import {
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { SortDirection } from './constants';
@@ -197,7 +197,7 @@ export default {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
- createFlash({
+ createAlert({
message: __('An error occurred while parsing recent searches'),
});
@@ -346,6 +346,11 @@ export default {
:suggestions-list-class="suggestionsListClass"
:search-button-attributes="searchButtonAttributes"
:search-input-attributes="searchInputAttributes"
+ :recent-searches-header="__('Recent searches')"
+ :clear-button-title="__('Clear')"
+ :close-button-title="__('Close')"
+ :clear-recent-searches-text="__('Clear recent searches')"
+ :no-recent-searches-text="__(`You don't have any recent searches`)"
class="flex-grow-1"
@history-item-selected="handleHistoryItemSelected"
@clear="onClear"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
index 7c4e372dda1..8a6053b7001 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/store/modules/filters/actions.js
@@ -1,5 +1,5 @@
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -24,7 +24,7 @@ export function fetchBranches({ commit, state }, search = '') {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_BRANCHES_ERROR, status);
- createFlash({
+ createAlert({
message: __('Failed to load branches. Please try again.'),
});
});
@@ -43,7 +43,7 @@ export const fetchMilestones = ({ commit, state }, searchTitle = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_MILESTONES_ERROR, status);
- createFlash({
+ createAlert({
message: __('Failed to load milestones. Please try again.'),
});
});
@@ -61,7 +61,7 @@ export const fetchLabels = ({ commit, state }, search = '') => {
.catch(({ response }) => {
const { status } = response;
commit(types.RECEIVE_LABELS_ERROR, status);
- createFlash({
+ createAlert({
message: __('Failed to load labels. Please try again.'),
});
});
@@ -86,7 +86,7 @@ function fetchUser(options = {}) {
.catch(({ response }) => {
const { status } = response;
commit(`RECEIVE_${action}_ERROR`, status);
- createFlash({
+ createAlert({
message: errorMessage,
});
});
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
index 848c49c48c7..7c184a3c391 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
@@ -1,7 +1,7 @@
<script>
import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { compact } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { DEFAULT_NONE_ANY } from '../constants';
@@ -65,7 +65,7 @@ export default {
this.authors = Array.isArray(res) ? compact(res) : compact(res.data);
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching users.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
index aa5161ca93c..741395b3193 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -46,7 +46,7 @@ export default {
this.branches = data;
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching branches.') });
+ createAlert({ message: __('There was a problem fetching branches.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
index adfe0559b62..d34cfb922a9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { ITEM_TYPE } from '~/groups/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmContactsQuery from '../queries/search_crm_contacts.query.graphql';
@@ -81,7 +81,7 @@ export default {
: data[this.namespace]?.contacts.nodes;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching CRM contacts.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
index e6ab944449e..c7c9350ee93 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { ITEM_TYPE } from '~/groups/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmOrganizationsQuery from '../queries/search_crm_organizations.query.graphql';
@@ -78,7 +78,7 @@ export default {
: data[this.namespace]?.organizations.nodes;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching CRM organizations.'),
}),
)
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 210d814d22a..929823f7308 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
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { DEFAULT_NONE_ANY } from '../constants';
@@ -48,7 +48,7 @@ export default {
this.emojis = Array.isArray(response) ? response : response.data;
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching emojis.') });
+ createAlert({ message: __('There was a problem fetching emojis.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 178c57a5666..bce0c11aafd 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -1,7 +1,7 @@
<script>
import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -81,7 +81,7 @@ export default {
this.labels = Array.isArray(res) ? res : res.data;
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('There was a problem fetching labels.'),
}),
)
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index 69265d0fdc9..b9ee4d51db1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { sortMilestonesByDueDate } from '~/milestones/utils';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -65,7 +65,7 @@ export default {
}
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching milestones.') });
+ createAlert({ message: __('There was a problem fetching milestones.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
index 9e68c92af5d..59701b4959e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { DEFAULT_NONE_ANY } from '../constants';
@@ -47,7 +47,7 @@ export default {
this.releases = response;
})
.catch(() => {
- createFlash({ message: __('There was a problem fetching releases.') });
+ createAlert({ message: __('There was a problem fetching releases.') });
})
.finally(() => {
this.loading = false;
diff --git a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
index 72148a0aa7c..c2be5e4f7a1 100644
--- a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
+++ b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
@@ -1,8 +1,10 @@
<script>
import { GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
const STATUS_TYPES = {
SUCCESS: 'success',
@@ -10,11 +12,14 @@ const STATUS_TYPES = {
DANGER: 'danger',
};
+const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
export default {
name: 'GitlabVersionCheck',
components: {
GlBadge,
},
+ mixins: [Tracking.mixin()],
props: {
size: {
type: String,
@@ -50,6 +55,10 @@ export default {
.then((res) => {
if (res.data) {
this.status = res.data.severity;
+
+ this.track('rendered_version_badge', {
+ label: this.title,
+ });
}
})
.catch(() => {
@@ -57,12 +66,24 @@ export default {
this.status = null;
});
},
+ onClick() {
+ this.track('click_version_badge', { label: this.title });
+ },
},
+ UPGRADE_DOCS_URL,
};
</script>
<template>
- <gl-badge v-if="status" class="version-check-badge" :variant="status" :size="size">{{
- title
- }}</gl-badge>
+ <!-- TODO: remove the span element once bootstrap-vue is updated to version 2.21.1 -->
+ <!-- TODO: https://github.com/bootstrap-vue/bootstrap-vue/issues/6219 -->
+ <span v-if="status" data-testid="badge-click-wrapper" @click="onClick">
+ <gl-badge
+ :href="$options.UPGRADE_DOCS_URL"
+ class="version-check-badge"
+ :variant="status"
+ :size="size"
+ >{{ title }}</gl-badge
+ >
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/utils.js b/app/assets/javascripts/vue_shared/components/group_select/utils.js
new file mode 100644
index 00000000000..0a4622269f4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/group_select/utils.js
@@ -0,0 +1,15 @@
+import Api from '~/api';
+
+export const groupsPath = (groupsFilter, parentGroupID) => {
+ if (groupsFilter !== undefined && parentGroupID === undefined) {
+ throw new Error('Cannot use groupsFilter without a parentGroupID');
+ }
+ switch (groupsFilter) {
+ case 'descendant_groups':
+ return Api.descendantGroupsPath.replace(':id', parentGroupID);
+ case 'subgroups':
+ return Api.subgroupsPath.replace(':id', parentGroupID);
+ default:
+ return Api.groupsPath;
+ }
+};
diff --git a/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
new file mode 100644
index 00000000000..d80c1ff8b0c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/constants.js
@@ -0,0 +1,12 @@
+import { issuableTypes } from '~/boards/constants';
+import blockingIssuesQuery from './graphql/blocking_issues.query.graphql';
+import blockingEpicsQuery from './graphql/blocking_epics.query.graphql';
+
+export const blockingIssuablesQueries = {
+ [issuableTypes.issue]: {
+ query: blockingIssuesQuery,
+ },
+ [issuableTypes.epic]: {
+ query: blockingEpicsQuery,
+ },
+};
diff --git a/app/assets/javascripts/boards/graphql/board_blocking_epics.query.graphql b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_epics.query.graphql
index 071a6d7410f..4b9a9243052 100644
--- a/app/assets/javascripts/boards/graphql/board_blocking_epics.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_epics.query.graphql
@@ -1,4 +1,4 @@
-query BoardBlockingEpics($fullPath: ID!, $iid: ID) {
+query BlockingEpics($fullPath: ID!, $iid: ID) {
group(fullPath: $fullPath) {
id
issuable: epic(iid: $iid) {
diff --git a/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_issues.query.graphql
index 01fab571733..279c2202740 100644
--- a/app/assets/javascripts/boards/graphql/board_blocking_issues.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/graphql/blocking_issues.query.graphql
@@ -1,4 +1,4 @@
-query BoardBlockingIssues($id: IssueID!) {
+query BlockingIssues($id: IssueID!) {
issuable: issue(id: $id) {
id
blockingIssuables: blockedByIssues {
diff --git a/app/assets/javascripts/boards/components/board_blocked_icon.vue b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
index 3f8a596abd8..253aca8837d 100644
--- a/app/assets/javascripts/boards/components/board_blocked_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue
@@ -1,10 +1,11 @@
<script>
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
-import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
+import { issuableTypes } from '~/boards/constants';
import { TYPE_ISSUE, TYPE_EPIC } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { truncate } from '~/lib/utils/text_utility';
import { __, n__, s__, sprintf } from '~/locale';
+import { blockingIssuablesQueries } from './constants';
export default {
i18n: {
@@ -28,7 +29,6 @@ export default {
GlLink,
GlLoadingIcon,
},
- blockingIssuablesQueries,
props: {
item: {
type: Object,
@@ -169,8 +169,8 @@ export default {
:id="glIconId"
ref="icon"
:name="blockIcon"
- class="issue-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500"
- data-testid="issue-blocked-icon"
+ class="issuable-blocked-icon gl-mr-2 gl-cursor-pointer gl-text-red-500"
+ data-testid="issuable-blocked-icon"
@mouseenter="handleMouseEnter"
/>
<gl-popover :target="glIconId" placement="top">
@@ -182,12 +182,19 @@ export default {
<p class="gl-mt-4 gl-mb-0 gl-font-small">{{ loadingMessage }}</p>
</template>
<template v-else>
- <ul class="gl-list-style-none gl-p-0">
- <li v-for="issuable in displayedIssuables" :key="issuable.id">
+ <ul class="gl-list-style-none gl-p-0 gl-mb-0">
+ <li v-for="(issuable, index) in displayedIssuables" :key="issuable.id">
<gl-link :href="issuable.webUrl" class="gl-text-blue-500! gl-font-sm">{{
issuable.reference
}}</gl-link>
- <p class="gl-mb-3 gl-display-block!" data-testid="issuable-title">
+ <p
+ class="gl-display-block!"
+ :class="{
+ 'gl-mb-3': index < displayedIssuables.length - 1,
+ 'gl-mb-0': index === displayedIssuables.length - 1,
+ }"
+ data-testid="issuable-title"
+ >
{{ issuable.title }}
</p>
</li>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index 926034efd10..caec49c557a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -51,6 +51,7 @@ export default {
<gl-dropdown
:text="dropdownText"
:disabled="disabled"
+ size="small"
boundary="window"
right
lazy
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 32b3a0e22c2..657e4498b53 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { debounce, unescape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
@@ -272,7 +272,7 @@ export default {
this.fetchMarkdown()
.then((data) => this.renderMarkdown(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading markdown preview'),
}),
);
@@ -315,7 +315,7 @@ export default {
this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error rendering Markdown preview'),
}),
);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 458dfe0ed23..89fffdedbfd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -7,6 +7,8 @@ import {
ITALIC_TEXT,
STRIKETHROUGH_TEXT,
LINK_TEXT,
+ INDENT_LINE,
+ OUTDENT_LINE,
} from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
@@ -68,12 +70,15 @@ export default {
},
computed: {
mdTable() {
+ const header = s__('MarkdownEditor|header');
+ const divider = '-'.repeat(header.length);
+ const cell = ' '.repeat(header.length);
+
return [
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- '| header | header |', // eslint-disable-line @gitlab/require-i18n-strings
- '| ------ | ------ |',
- '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings
- '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings
+ `| ${header} | ${header} |`,
+ `| ${divider} | ${divider} |`,
+ `| ${cell} | ${cell} |`,
+ `| ${cell} | ${cell} |`,
].join('\n');
},
mdSuggestion() {
@@ -82,7 +87,8 @@ export default {
);
},
mdCollapsibleSection() {
- return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n');
+ 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
@@ -170,6 +176,8 @@ export default {
italic: keysFor(ITALIC_TEXT),
strikethrough: keysFor(STRIKETHROUGH_TEXT),
link: keysFor(LINK_TEXT),
+ indent: keysFor(INDENT_LINE),
+ outdent: keysFor(OUTDENT_LINE),
},
i18n: {
writeTabTitle: __('Write'),
@@ -235,6 +243,7 @@ export default {
variant="confirm"
category="primary"
size="small"
+ data-qa-selector="dismiss_suggestion_popover_button"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
@@ -318,6 +327,32 @@ export default {
icon="list-task"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('indent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.indent"
+ command="indentLines"
+ icon="list-indent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('outdent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.outdent"
+ command="outdentLines"
+ icon="list-outdent"
+ />
+ <toolbar-button
v-if="!restrictedToolBarItems.includes('collapsible-section')"
:tag="mdCollapsibleSection"
:prepend="true"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
new file mode 100644
index 00000000000..b38772d5aa5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -0,0 +1,216 @@
+<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 { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
+import MarkdownField from './field.vue';
+
+export default {
+ components: {
+ MarkdownField,
+ LocalStorageSync,
+ GlSegmentedControl,
+ ContentEditor: () =>
+ import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
+ ),
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ renderMarkdownPath: {
+ type: String,
+ required: true,
+ },
+ markdownDocsPath: {
+ type: String,
+ required: true,
+ },
+ quickActionsDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ uploadsPath: {
+ type: String,
+ required: false,
+ default: () => window.uploads_path,
+ },
+ enableContentEditor: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ formFieldId: {
+ type: String,
+ required: true,
+ },
+ formFieldName: {
+ type: String,
+ required: true,
+ },
+ enablePreview: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ formFieldPlaceholder: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ formFieldAriaLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initOnAutofocus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ supportsQuickActions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ editingMode: EDITING_MODE_MARKDOWN_FIELD,
+ switchEditingControlEnabled: true,
+ autofocus: this.initOnAutofocus,
+ };
+ },
+ computed: {
+ isContentEditorActive() {
+ return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
+ },
+ contentEditorAutofocus() {
+ // Match textarea focus behavior
+ return this.autofocus ? 'end' : false;
+ },
+ },
+ mounted() {
+ this.autofocusTextarea(this.editingMode);
+ },
+ methods: {
+ updateMarkdownFromContentEditor({ markdown }) {
+ this.$emit('input', markdown);
+ },
+ 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.notifyEditingModeChange(editingMode);
+ this.enableAutofocus(editingMode);
+ },
+ onEditingModeRestored(editingMode) {
+ this.notifyEditingModeChange(editingMode);
+ },
+ notifyEditingModeChange(editingMode) {
+ this.$emit(editingMode);
+ },
+ enableAutofocus(editingMode) {
+ this.autofocus = true;
+ this.autofocusTextarea(editingMode);
+ },
+ autofocusTextarea(editingMode) {
+ if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) {
+ this.$refs.textarea.focus();
+ }
+ },
+ },
+ 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"
+ @input="onEditingModeRestored"
+ />
+ <markdown-field
+ v-if="!isContentEditorActive"
+ :markdown-preview-path="renderMarkdownPath"
+ can-attach-file
+ :enable-autocomplete="enableAutocomplete"
+ :textarea-value="value"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :uploads-path="uploadsPath"
+ :enable-preview="enablePreview"
+ class="bordered-box"
+ >
+ <template #textarea>
+ <textarea
+ :id="formFieldId"
+ ref="textarea"
+ :value="value"
+ :name="formFieldName"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ :data-supports-quick-actions="supportsQuickActions"
+ data-qa-selector="markdown_editor_form_field"
+ :aria-label="formFieldAriaLabel"
+ :placeholder="formFieldPlaceholder"
+ @input="updateMarkdownFromMarkdownField"
+ @keydown="$emit('keydown', $event)"
+ >
+ </textarea>
+ </template>
+ </markdown-field>
+ <div v-else>
+ <content-editor
+ :render-markdown="renderMarkdown"
+ :uploads-path="uploadsPath"
+ :markdown="value"
+ :autofocus="contentEditorAutofocus"
+ @change="updateMarkdownFromContentEditor"
+ @loading="disableSwitchEditingControl"
+ @loadingSuccess="enableSwitchEditingControl"
+ @loadingError="enableSwitchEditingControl"
+ @keydown="$emit('keydown', $event)"
+ />
+ <input
+ :id="formFieldId"
+ :value="value"
+ :name="formFieldName"
+ data-qa-selector="markdown_editor_form_field"
+ type="hidden"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 7646a8718d6..855c7a449c4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -139,7 +139,7 @@ export default {
</script>
<template>
- <div class="md-suggestion-header border-bottom-0 gl-mt-3">
+ <div class="md-suggestion-header border-bottom-0 gl-px-4 gl-py-3">
<div class="js-suggestion-diff-header gl-font-weight-bold">
{{ __('Suggested change') }}
<a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
@@ -162,6 +162,7 @@ export default {
<gl-button
class="btn-inverted js-remove-from-batch-btn btn-grouped"
:disabled="isApplying"
+ size="small"
@click="removeSuggestionFromBatch"
>
{{ __('Remove from batch') }}
@@ -172,6 +173,7 @@ export default {
class="btn-inverted js-add-to-batch-btn btn-grouped"
data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
+ size="small"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 9b81444fc04..30d72332c90 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
@@ -91,7 +91,7 @@ export default {
const suggestionElements = container.querySelectorAll('.js-render-suggestion');
if (this.lineType === 'old') {
- createFlash({
+ createAlert({
message: __('Unable to apply suggestions to a deleted line.'),
parent: this.$el,
});
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 49217e38a1b..5ca21522d33 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -47,6 +47,11 @@ export default {
required: false,
default: 0,
},
+ command: {
+ type: String,
+ required: false,
+ default: '',
+ },
/**
* A string (or an array of strings) of
@@ -81,6 +86,7 @@ export default {
:data-md-tag-content="tagContent"
:data-md-prepend="prepend"
:data-md-shortcuts="shortcutsString"
+ :data-md-command="command"
:title="buttonTitle"
:aria-label="buttonTitle"
:icon="icon"
diff --git a/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js b/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js
index 832fb891838..1c4e8d332a9 100644
--- a/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/metric_images/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import * as types from './mutation_types';
@@ -11,7 +11,7 @@ export const fetchImagesFactory = (service) => async ({ state, commit }) => {
commit(types.RECEIVE_METRIC_IMAGES_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_IMAGES_ERROR);
- createFlash({ message: s__('MetricImages|There was an issue loading metric images.') });
+ createAlert({ message: s__('MetricImages|There was an issue loading metric images.') });
}
};
@@ -34,7 +34,7 @@ export const uploadImageFactory = (service) => async (
commit(types.RECEIVE_METRIC_UPLOAD_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
- createFlash({ message: s__('MetricImages|There was an issue uploading your image.') });
+ createAlert({ message: s__('MetricImages|There was an issue uploading your image.') });
}
};
@@ -57,7 +57,7 @@ export const updateImageFactory = (service) => async (
commit(types.RECEIVE_METRIC_UPDATE_SUCCESS, response);
} catch (error) {
commit(types.RECEIVE_METRIC_UPLOAD_ERROR);
- createFlash({ message: s__('MetricImages|There was an issue updating your image.') });
+ createAlert({ message: s__('MetricImages|There was an issue updating your image.') });
}
};
@@ -68,7 +68,7 @@ export const deleteImageFactory = (service) => async ({ state, commit }, imageId
await service.deleteMetricImage({ imageId, id: projectId, modelIid });
commit(types.RECEIVE_METRIC_DELETE_SUCCESS, imageId);
} catch (error) {
- createFlash({ message: s__('MetricImages|There was an issue deleting the image.') });
+ createAlert({ message: s__('MetricImages|There was an issue deleting the image.') });
}
};
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index d4f50e347cb..41c92fdba4f 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -61,6 +61,11 @@ export default {
required: false,
default: 'primary',
},
+ size: {
+ type: String,
+ required: false,
+ default: 'medium',
+ },
},
computed: {
modalDomId() {
@@ -103,6 +108,9 @@ export default {
:title="title"
:aria-label="title"
:category="category"
+ :size="size"
icon="copy-to-clipboard"
- />
+ >
+ <slot></slot>
+ </gl-button>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
index e9f278a5db5..ba9edc7620a 100644
--- a/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue
+++ b/app/assets/javascripts/vue_shared/components/namespace_select/namespace_select_deprecated.vue
@@ -27,7 +27,7 @@ const filterByName = (data, searchTerm = '') => {
};
export default {
- name: 'NamespaceSelect',
+ name: 'NamespaceSelectDeprecated',
components: {
GlDropdown,
GlDropdownDivider,
@@ -78,7 +78,7 @@ export default {
required: false,
default: false,
},
- isLoadingMoreGroups: {
+ isLoading: {
type: Boolean,
required: false,
default: false,
@@ -152,7 +152,12 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="selectedNamespaceText" :block="fullWidth" data-qa-selector="namespaces_list">
+ <gl-dropdown
+ :text="selectedNamespaceText"
+ :block="fullWidth"
+ data-qa-selector="namespaces_list"
+ @show="$emit('show')"
+ >
<template #header>
<gl-search-box-by-type
v-model.trim="searchTerm"
@@ -201,8 +206,7 @@ export default {
>{{ item.humanName }}</gl-dropdown-item
>
</div>
- <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')">
- <gl-loading-icon v-if="isLoadingMoreGroups" class="gl-mb-3" size="sm" />
- </gl-intersection-observer>
+ <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" />
+ <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="$emit('load-more-groups')" />
</gl-dropdown>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index 0cb4a5bc39f..cf34a60c363 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -50,29 +50,19 @@ export default {
renderedNote() {
return renderMarkdown(this.note.body);
},
- avatarSize() {
- if (this.line && !this.isOverviewTab) {
- return 24;
- }
-
- return {
- default: 24,
- md: 32,
- };
- },
},
};
</script>
<template>
- <timeline-entry-item class="note note-wrapper being-posted fade-in-half">
- <div class="timeline-icon">
- <gl-avatar-link class="gl-mr-3" :href="getUserData.path">
+ <timeline-entry-item class="note note-wrapper note-comment being-posted fade-in-half">
+ <div class="timeline-avatar gl-float-left">
+ <gl-avatar-link :href="getUserData.path">
<gl-avatar
:src="getUserData.avatar_url"
:entity-name="getUserData.username"
:alt="getUserData.name"
- :size="avatarSize"
+ :size="32"
/>
</gl-avatar-link>
</div>
@@ -85,8 +75,10 @@ export default {
</a>
</div>
</div>
- <div class="note-body">
- <div v-safe-html="renderedNote" class="note-text md"></div>
+ <div class="timeline-discussion-body">
+ <div class="note-body">
+ <div v-safe-html="renderedNote" class="note-text md"></div>
+ </div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index 2206ae98c73..e091fe74717 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -16,7 +16,7 @@ export default {
<div class="timeline-icon"></div>
<div class="timeline-content">
<div class="note-header"></div>
- <div class="note-body"><gl-skeleton-loader /></div>
+ <div class="note-body gl-mt-4"><gl-skeleton-loader /></div>
</div>
</timeline-entry-item>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 7e99f1b01b2..1ae5045b34f 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -129,7 +129,12 @@ export default {
<div v-safe-html:[$options.safeHtmlConfig]="iconHtml" class="timeline-icon"></div>
<div class="timeline-content">
<div class="note-header">
- <note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
+ <note-header
+ :author="note.author"
+ :created-at="note.created_at"
+ :note-id="note.id"
+ :is-system-note="true"
+ >
<span ref="gfm-content" v-safe-html="actionTextHtml"></span>
<template
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
@@ -141,7 +146,7 @@ export default {
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
- class="gl-vertical-align-text-bottom"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
@@ -150,7 +155,7 @@ export default {
:icon="showLines ? 'chevron-up' : 'chevron-down'"
variant="link"
data-testid="outdated-lines-change-btn"
- class="gl-vertical-align-text-bottom"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
@click="toggleDiff"
>
{{ __('Compare changes') }}
@@ -190,7 +195,7 @@ export default {
</div>
<div
v-if="lines.length && showLines"
- class="diff-content gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ class="diff-content outdated-lines-wrapper gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
>
<table
:class="$options.userColorSchemeClass"
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
index b7768cfa5b9..df1188d365b 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/constants.js
@@ -4,7 +4,7 @@ export const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
export const thClass = 'gl-hover-bg-blue-50';
export const bodyTrClass =
- 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-b-solid gl-hover-border-blue-200';
+ 'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-gray-50 gl-hover-border-b-solid';
export const defaultPageSize = 20;
diff --git a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
index 6867b5a75e3..a5027d2ca5c 100644
--- a/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
+++ b/app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue
@@ -275,7 +275,7 @@ export default {
<template>
<div class="incident-management-list">
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="$emit('error-alert-dismissed')">
- <p v-safe-html="serverErrorMessage || i18n.errorMsg"></p>
+ <span v-safe-html="serverErrorMessage || i18n.errorMsg"></span>
</gl-alert>
<div
diff --git a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
index b4d565991f5..c1246b2bf44 100644
--- a/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
+++ b/app/assets/javascripts/vue_shared/components/pagination_bar/pagination_bar.vue
@@ -2,6 +2,7 @@
import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
const DEFAULT_PAGE_SIZES = [20, 50, 100];
@@ -12,6 +13,7 @@ export default {
GlDropdownItem,
GlIcon,
GlSprintf,
+ LocalStorageSync,
},
props: {
pageInfo: {
@@ -23,6 +25,11 @@ export default {
type: Array,
default: () => DEFAULT_PAGE_SIZES,
},
+ storageKey: {
+ required: false,
+ type: String,
+ default: null,
+ },
},
computed: {
@@ -66,6 +73,12 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-center">
+ <local-storage-sync
+ v-if="storageKey"
+ :storage-key="storageKey"
+ :value="pageInfo.perPage"
+ @input="setPageSize"
+ />
<pagination-links :change="setPage" :page-info="pageInfo" class="gl-m-0" />
<gl-dropdown category="tertiary" class="gl-ml-auto" data-testid="page-size">
<template #button-content>
diff --git a/app/assets/javascripts/vue_shared/components/registry/history_item.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
index a60b630b207..384b084ce09 100644
--- a/app/assets/javascripts/vue_shared/components/registry/history_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
@@ -18,15 +18,15 @@ export default {
</script>
<template>
- <timeline-entry-item class="system-note note-wrapper gl-mb-6!">
+ <timeline-entry-item class="system-note note-wrapper">
<div class="timeline-icon">
<gl-icon :name="icon" />
</div>
<div class="timeline-content">
<div class="note-header">
- <span>
+ <div class="note-header-info">
<slot></slot>
- </span>
+ </div>
</div>
<div class="note-body">
<slot name="body"></slot>
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index 1948a6778f4..8c9c7c63db1 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -1,6 +1,7 @@
<script>
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { SORT_DIRECTION_UI } from '~/search/sort/constants';
const ASCENDING_ORDER = 'asc';
const DESCENDING_ORDER = 'desc';
@@ -52,6 +53,9 @@ export default {
return acc;
}, {});
},
+ sortDirectionData() {
+ return this.isSortAscending ? SORT_DIRECTION_UI.asc : SORT_DIRECTION_UI.desc;
+ },
},
methods: {
generateQueryData({ sorting = {}, filter = [] } = {}) {
@@ -119,6 +123,7 @@ export default {
data-testid="registry-sort-dropdown"
:text="sortText"
:is-ascending="isSortAscending"
+ :sort-direction-tool-tip="sortDirectionData.tooltip"
@sortDirectionChange="onDirectionChange"
>
<gl-sorting-item
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
index b61996cdcdb..e6c29e24f0c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
@@ -53,6 +53,11 @@ export default {
required: false,
default: false,
},
+ allowMultipleScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
variant: {
type: String,
required: false,
@@ -164,6 +169,7 @@ export default {
allowLabelCreate: this.allowLabelCreate,
allowMultiselect: this.allowMultiselect,
allowScopedLabels: this.allowScopedLabels,
+ allowMultipleScopedLabels: this.allowMultipleScopedLabels,
dropdownButtonText: this.dropdownButtonText,
selectedLabels: this.selectedLabels,
labelsFetchPath: this.labelsFetchPath,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index 0c697e624ab..2dab97826b9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -16,7 +16,7 @@ export const receiveLabelsSuccess = ({ commit }, labels) =>
commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
export const receiveLabelsFailure = ({ commit }) => {
commit(types.RECEIVE_SET_LABELS_FAILURE);
- createFlash({
+ createAlert({
message: __('Error fetching labels.'),
});
};
@@ -38,7 +38,7 @@ export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LA
export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
export const receiveCreateLabelFailure = ({ commit }) => {
commit(types.RECEIVE_CREATE_LABEL_FAILURE);
- createFlash({
+ createAlert({
message: __('Error creating label.'),
});
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 43b23994cdf..c85d9befcbb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -94,14 +94,13 @@ export default {
candidateLabel.indeterminate = false;
}
- if (isScopedLabel(candidateLabel)) {
+ if (isScopedLabel(candidateLabel) && !state.allowMultipleScopedLabels) {
const currentActiveScopedLabel = state.labels.find(
({ set, title }) =>
set &&
title !== candidateLabel.title &&
scopedLabelKey({ title }) === scopedLabelKey(candidateLabel),
);
-
if (currentActiveScopedLabel) {
currentActiveScopedLabel.set = false;
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index 5f344ae4214..ce93ad216ec 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -8,7 +8,7 @@ import {
GlLoadingIcon,
} from '@gitlab/ui';
import produce from 'immer';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { workspaceLabelsQueries } from '~/sidebar/constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
@@ -129,7 +129,7 @@ export default {
this.$emit('hideCreateView');
}
} catch {
- createFlash({ message: errorMessage });
+ createAlert({ message: errorMessage });
}
this.labelCreateInProgress = false;
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index 8d3d4d5f86a..1d854505d11 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { workspaceLabelsQueries } from '~/sidebar/constants';
@@ -62,7 +62,7 @@ export default {
},
update: (data) => data.workspace?.labels?.nodes || [],
error() {
- createFlash({ message: __('Error fetching labels.') });
+ createAlert({ message: __('Error fetching labels.') });
},
},
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 522fbc07f5e..0e8da7281d8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -2,7 +2,7 @@
import { debounce } from 'lodash';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issues/constants';
@@ -151,7 +151,7 @@ export default {
return data.workspace?.issuable;
},
error() {
- createFlash({ message: __('Error fetching labels.') });
+ createAlert({ message: __('Error fetching labels.') });
},
subscribeToMore: {
document() {
@@ -275,7 +275,7 @@ export default {
});
})
.catch((error) =>
- createFlash({
+ createAlert({
message: __('An error occurred while updating labels.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
index 445817d3e52..eae5e96ac46 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query issueParticipants($fullPath: ID!, $iid: String!) {
+query issueParticipants($fullPath: ID!, $iid: String!, $getStatus: Boolean = false) {
workspace: project(fullPath: $fullPath) {
id
issuable: issue(iid: $iid) {
@@ -9,7 +9,7 @@ query issueParticipants($fullPath: ID!, $iid: String!) {
participants {
nodes {
...User
- ...UserAvailability
+ ...UserAvailability @include(if: $getStatus)
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
index 05de680ab05..f087ca6c982 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
@@ -19,7 +19,7 @@ query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
}
}
userPermissions {
- updateMergeRequest
+ adminMergeRequest
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
index 3496d5f4a2e..2781ac71f31 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query getMrParticipants($fullPath: ID!, $iid: String!) {
+query getMrParticipants($fullPath: ID!, $iid: String!, $getStatus: Boolean = false) {
workspace: project(fullPath: $fullPath) {
id
issuable: mergeRequest(iid: $iid) {
@@ -9,7 +9,7 @@ query getMrParticipants($fullPath: ID!, $iid: String!) {
participants {
nodes {
...User
- ...UserAvailability
+ ...UserAvailability @include(if: $getStatus)
}
}
}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 257b9f57222..ffd0eea63a1 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,8 +1,6 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { setAttributes } from '~/lib/utils/dom_utils';
-import { BIDI_CHARS, BIDI_CHARS_CLASS_LIST, BIDI_CHAR_TOOLTIP } from '../constants';
export default {
directives: {
@@ -27,34 +25,6 @@ export default {
required: true,
},
},
- computed: {
- formattedContent() {
- let { content } = this;
-
- BIDI_CHARS.forEach((bidiChar) => {
- if (content.includes(bidiChar)) {
- content = content.replace(bidiChar, this.wrapBidiChar(bidiChar));
- }
- });
-
- return content;
- },
- },
- methods: {
- wrapBidiChar(bidiChar) {
- const span = document.createElement('span');
-
- setAttributes(span, {
- class: BIDI_CHARS_CLASS_LIST,
- title: BIDI_CHAR_TOOLTIP,
- 'data-testid': 'bidi-wrapper',
- });
-
- span.innerText = bidiChar;
-
- return span.outerHTML;
- },
- },
};
</script>
<template>
@@ -78,7 +48,7 @@ export default {
</div>
<pre
- class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-normal"
- ><code><span :id="`LC${number}`" v-safe-html="formattedContent" :lang="language" class="line" data-testid="content"></span></code></pre>
+ class="gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
+ ><code><span :id="`LC${number}`" v-safe-html="content" :lang="language" class="line" data-testid="content"></span></code></pre>
</div>
</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 30f57f506a6..a28460dd58e 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -1,5 +1,3 @@
-import { __ } from '~/locale';
-
// Language map from Rouge::Lexer to highlight.js
// Rouge::Lexer - We use it on the BE to determine the language of a source file (https://github.com/rouge-ruby/rouge/blob/master/docs/Languages.md).
// Highlight.js - We use it on the FE to highlight the syntax of a source file (https://github.com/highlightjs/highlight.js/tree/main/src/languages).
@@ -139,13 +137,6 @@ export const BIDI_CHARS = [
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_COMMENT_SELECTOR = 'hljs-comment';
+export const BIDI_CHAR_TOOLTIP = 'Potentially unwanted character detected: Unicode BiDi Control';
export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
-
-export const NPM_URL = 'https://npmjs.com/package';
-export const GEM_URL = 'https://rubygems.org/gems';
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
index 5d24a3d110b..d694adf7147 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/index.js
@@ -1,6 +1,8 @@
-import { HLJS_ON_AFTER_HIGHLIGHT } from '../constants';
-import wrapComments from './wrap_comments';
+import wrapChildNodes from './wrap_child_nodes';
import linkDependencies from './link_dependencies';
+import wrapBidiChars from './wrap_bidi_chars';
+
+export const HLJS_ON_AFTER_HIGHLIGHT = 'after:highlight';
/**
* Registers our plugins for Highlight.js
@@ -10,7 +12,8 @@ import linkDependencies from './link_dependencies';
* @param {Object} hljs - the Highlight.js instance.
*/
export const registerPlugins = (hljs, fileType, rawContent) => {
- hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapComments });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapChildNodes });
+ hljs.addPlugin({ [HLJS_ON_AFTER_HIGHLIGHT]: wrapBidiChars });
hljs.addPlugin({
[HLJS_ON_AFTER_HIGHLIGHT]: (result) => linkDependencies(result, fileType, rawContent),
});
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
index dbe6812cf16..49704421d6e 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/dependency_linker_util.js
@@ -1,16 +1,7 @@
import { escape } from 'lodash';
-import { setAttributes } from '~/lib/utils/dom_utils';
-export const createLink = (href, innerText) => {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- const rel = 'nofollow noreferrer noopener';
- const link = document.createElement('a');
-
- setAttributes(link, { href: escape(href), rel });
- link.textContent = innerText;
-
- return link.outerHTML;
-};
+export const createLink = (href, innerText) =>
+ `<a href="${escape(href)}" rel="nofollow noreferrer noopener">${escape(innerText)}</a>`;
export const generateHLJSOpenTag = (type, delimiter = '&quot;') =>
`<span class="hljs-${escape(type)}">${delimiter}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
index 35de8fd13d6..46c9dc38300 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/gemspec_linker.js
@@ -1,7 +1,6 @@
-import { joinPaths } from '~/lib/utils/url_utility';
-import { GEM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+const GEM_URL = 'https://rubygems.org/gems/';
const methodRegex = '.*add_dependency.*|.*add_runtime_dependency.*|.*add_development_dependency.*';
const openTagRegex = generateHLJSOpenTag('string', '(&.*;)');
const closeTagRegex = '&.*</span>';
@@ -24,7 +23,7 @@ const DEPENDENCY_REGEX = new RegExp(
const handleReplace = (method, delimiter, packageName, closeTag, rest) => {
// eslint-disable-next-line @gitlab/require-i18n-strings
const openTag = generateHLJSOpenTag('string linked', delimiter);
- const href = joinPaths(GEM_URL, packageName);
+ const href = `${GEM_URL}${packageName}`;
const packageLink = createLink(href, packageName);
return `${method}${openTag}${packageLink}${closeTag}${rest}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
index 3c6fc23c138..4bfd5ec2431 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/package_json_linker.js
@@ -1,8 +1,7 @@
import { unescape } from 'lodash';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { NPM_URL } from '../../constants';
import { createLink, generateHLJSOpenTag } from './dependency_linker_util';
+const NPM_URL = 'https://npmjs.com/package/';
const attrOpenTag = generateHLJSOpenTag('attr');
const stringOpenTag = generateHLJSOpenTag('string');
const closeTag = '&quot;</span>';
@@ -20,7 +19,7 @@ const DEPENDENCY_REGEX = new RegExp(
const handleReplace = (original, packageName, version, dependenciesToLink) => {
const unescapedPackageName = unescape(packageName);
const unescapedVersion = unescape(version);
- const href = joinPaths(NPM_URL, unescapedPackageName);
+ const href = `${NPM_URL}${unescapedPackageName}`;
const packageLink = createLink(href, unescapedPackageName);
const versionLink = createLink(href, unescapedVersion);
const closeAndOpenTag = `${closeTag}: ${attrOpenTag}`;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
new file mode 100644
index 00000000000..3b6cd96ef78
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_bidi_chars.js
@@ -0,0 +1,30 @@
+import {
+ BIDI_CHARS,
+ BIDI_CHARS_CLASS_LIST,
+ BIDI_CHAR_TOOLTIP,
+} from '~/vue_shared/components/source_viewer/constants';
+
+/**
+ * Highlight.js plugin for wrapping BIDI chars.
+ * This ensures potentially dangerous BIDI characters are highlighted.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+
+function wrapBidiChar(bidiChar) {
+ return `<span class="${BIDI_CHARS_CLASS_LIST}" title="${BIDI_CHAR_TOOLTIP}">${bidiChar}</span>`;
+}
+
+export default (result) => {
+ let { value } = result;
+ BIDI_CHARS.forEach((bidiChar) => {
+ if (value.includes(bidiChar)) {
+ value = value.replace(bidiChar, wrapBidiChar(bidiChar));
+ }
+ });
+
+ // eslint-disable-next-line no-param-reassign
+ result.value = value; // Highlight.js expects the result param to be mutated for plugins to work
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
new file mode 100644
index 00000000000..e0ba4b730a7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_child_nodes.js
@@ -0,0 +1,45 @@
+import { escape } from 'lodash';
+
+/**
+ * Highlight.js plugin for wrapping nodes with the correct selectors to ensure
+ * child-elements are highlighted correctly after we split up the result into chunks and lines.
+ *
+ * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
+ *
+ * @param {Object} Result - an object that represents the highlighted result from Highlight.js
+ */
+const newlineRegex = /\r?\n/;
+const generateClassName = (suffix) => (suffix ? `hljs-${escape(suffix)}` : '');
+const generateCloseTag = (includeClose) => (includeClose ? '</span>' : '');
+const generateHLJSTag = (kind, content = '', includeClose) =>
+ `<span class="${generateClassName(kind)}">${escape(content)}${generateCloseTag(includeClose)}`;
+
+const format = (node, kind = '') => {
+ let buffer = '';
+
+ if (typeof node === 'string') {
+ buffer += node
+ .split(newlineRegex)
+ .map((newline) => generateHLJSTag(kind, newline, true))
+ .join('\n');
+ } else if (node.kind) {
+ const { children } = node;
+ if (children.length && children.length === 1) {
+ buffer += format(children[0], node.kind);
+ } else {
+ buffer += generateHLJSTag(node.kind);
+ children.forEach((subChild) => {
+ buffer += format(subChild, node.kind);
+ });
+ buffer += `</span>`;
+ }
+ }
+
+ return buffer;
+};
+
+export default (result) => {
+ // NOTE: We're using the private Emitter API here as we expect the Emitter API to be publicly available soon (https://github.com/highlightjs/highlight.js/issues/3621)
+ // eslint-disable-next-line no-param-reassign, no-underscore-dangle
+ result.value = result._emitter.rootNode.children.reduce((val, node) => val + format(node), ''); // Highlight.js expects the result param to be mutated for plugins to work
+};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
deleted file mode 100644
index 8b52df83fdf..00000000000
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/wrap_comments.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { HLJS_COMMENT_SELECTOR } from '../constants';
-
-const createWrapper = (content) => {
- const span = document.createElement('span');
- span.className = HLJS_COMMENT_SELECTOR;
-
- // eslint-disable-next-line no-unsanitized/property
- span.innerHTML = content;
- return span.outerHTML;
-};
-
-/**
- * Highlight.js plugin for wrapping multi-line comments in the `hljs-comment` class.
- * This ensures that multi-line comments are rendered correctly in the GitLab UI.
- *
- * Plugin API: https://github.com/highlightjs/highlight.js/blob/main/docs/plugin-api.rst
- *
- * @param {Object} Result - an object that represents the highlighted result from Highlight.js
- */
-export default (result) => {
- if (!result.value.includes(HLJS_COMMENT_SELECTOR)) return;
-
- let wrapComment = false;
-
- // eslint-disable-next-line no-param-reassign
- result.value = result.value // Highlight.js expects the result param to be mutated for plugins to work
- .split('\n')
- .map((lineContent) => {
- const includesClosingTag = lineContent.includes('</span>');
- if (lineContent.includes(HLJS_COMMENT_SELECTOR) && !includesClosingTag) {
- wrapComment = true;
- return lineContent;
- }
- const line = wrapComment ? createWrapper(lineContent) : lineContent;
- if (includesClosingTag) {
- wrapComment = false;
- }
- return line;
- })
- .join('\n');
-};
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 9c6c12eac7d..536b2c8a281 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
@@ -53,7 +53,7 @@ export default {
},
computed: {
splitContent() {
- return this.content.split('\n');
+ return this.content.split(/\r?\n/);
},
lineNumbers() {
return this.splitContent.length;
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
new file mode 100644
index 00000000000..535e857d7a9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight.js
@@ -0,0 +1,10 @@
+import { highlight } from './highlight_utils';
+
+/**
+ * A webworker for highlighting large amounts of content with Highlight.js
+ */
+// eslint-disable-next-line no-restricted-globals
+self.addEventListener('message', ({ data: { fileType, content, language } }) => {
+ // eslint-disable-next-line no-restricted-globals
+ self.postMessage(highlight(fileType, content, language));
+});
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
new file mode 100644
index 00000000000..0da57f9e6fa
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
@@ -0,0 +1,15 @@
+import hljs from 'highlight.js/lib/core';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import { registerPlugins } from '../plugins/index';
+
+const initHighlightJs = async (fileType, content, language) => {
+ const languageDefinition = await languageLoader[language]();
+
+ registerPlugins(hljs, fileType, content);
+ hljs.registerLanguage(language, languageDefinition.default);
+};
+
+export const highlight = (fileType, content, language) => {
+ initHighlightJs(fileType, content, language);
+ return hljs.highlight(content, { language }).value;
+};
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
deleted file mode 100644
index ce65266cbc9..00000000000
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import { secondsToHours } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
-
-export default {
- name: 'TimezoneDropdown',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- },
- directives: {
- autofocusonshow,
- },
- props: {
- value: {
- type: String,
- required: true,
- default: '',
- },
- timezoneData: {
- type: Array,
- required: true,
- default: () => [],
- },
- },
- data() {
- return {
- searchTerm: '',
- };
- },
- tranlations: {
- noResultsText: __('No matching results'),
- },
- computed: {
- timezones() {
- return this.timezoneData.map((timezone) => ({
- formattedTimezone: this.formatTimezone(timezone),
- identifier: timezone.identifier,
- }));
- },
- filteredResults() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.timezones.filter((timezone) =>
- timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm),
- );
- },
- selectedTimezoneLabel() {
- return this.value || __('Select timezone');
- },
- },
- methods: {
- selectTimezone(selectedTimezone) {
- this.$emit('input', selectedTimezone);
- this.searchTerm = '';
- },
- isSelected(timezone) {
- return this.value === timezone.formattedTimezone;
- },
- formatTimezone(item) {
- return `[UTC ${secondsToHours(item.offset)}] ${item.name}`;
- },
- },
-};
-</script>
-<template>
- <gl-dropdown :text="selectedTimezoneLabel" 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"
- :key="timezone.formattedTimezone"
- :is-checked="isSelected(timezone)"
- is-check-item
- @click="selectTimezone(timezone)"
- >
- {{ timezone.formattedTimezone }}
- </gl-dropdown-item>
- <gl-dropdown-item
- v-if="!filteredResults.length"
- class="gl-pointer-events-none"
- data-testid="noMatchingResults"
- >
- {{ $options.tranlations.noResultsText }}
- </gl-dropdown-item>
- </gl-dropdown>
-</template>
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
new file mode 100644
index 00000000000..423501265d7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
@@ -0,0 +1,119 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { __ } from '~/locale';
+import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
+import { formatTimezone } from '~/lib/utils/datetime_utility';
+
+export default {
+ name: 'TimezoneDropdown',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ directives: {
+ autofocusonshow,
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ default: '',
+ },
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ timezoneData: {
+ type: Array,
+ required: true,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ tzValue: this.initialTimezone(this.timezoneData, this.value),
+ };
+ },
+ translations: {
+ noResultsText: __('No matching results'),
+ },
+ computed: {
+ timezones() {
+ return this.timezoneData.map((timezone) => ({
+ formattedTimezone: formatTimezone(timezone),
+ identifier: timezone.identifier,
+ }));
+ },
+ filteredResults() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.timezones.filter((timezone) =>
+ timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ selectedTimezoneLabel() {
+ return this.tzValue || __('Select timezone');
+ },
+ timezoneIdentifier() {
+ return this.tzValue
+ ? this.timezones.find((timezone) => timezone.formattedTimezone === this.tzValue).identifier
+ : undefined;
+ },
+ },
+ methods: {
+ selectTimezone(selectedTimezone) {
+ this.tzValue = selectedTimezone.formattedTimezone;
+ this.$emit('input', selectedTimezone);
+ this.searchTerm = '';
+ },
+ isSelected(timezone) {
+ return this.tzValue === timezone.formattedTimezone;
+ },
+ initialTimezone(timezones, value) {
+ if (!value) {
+ return undefined;
+ }
+
+ const initialTimezone = timezones.find((timezone) => timezone.identifier === value);
+
+ if (initialTimezone) {
+ return formatTimezone(initialTimezone);
+ }
+
+ return undefined;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <input
+ v-if="name"
+ id="user_timezone"
+ :name="name"
+ :value="timezoneIdentifier || value"
+ type="hidden"
+ />
+ <gl-dropdown :text="selectedTimezoneLabel" 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"
+ :key="timezone.formattedTimezone"
+ :is-checked="isSelected(timezone)"
+ is-check-item
+ @click="selectTimezone(timezone)"
+ >
+ {{ timezone.formattedTimezone }}
+ </gl-dropdown-item>
+ <gl-dropdown-item
+ v-if="!filteredResults.length"
+ class="gl-pointer-events-none"
+ data-testid="noMatchingResults"
+ >
+ {{ $options.translations.noResultsText }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue
index 925c6008836..bd5b7b77017 100644
--- a/app/assets/javascripts/vue_shared/components/url_sync.vue
+++ b/app/assets/javascripts/vue_shared/components/url_sync.vue
@@ -1,6 +1,9 @@
<script>
import { historyPushState } from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
+
+export const URL_SET_PARAMS_STRATEGY = 'set';
+export const URL_MERGE_PARAMS_STRATEGY = 'merge';
/**
* Renderless component to update the query string,
@@ -15,6 +18,12 @@ export default {
required: false,
default: null,
},
+ urlParamsUpdateStrategy: {
+ type: String,
+ required: false,
+ default: URL_MERGE_PARAMS_STRATEGY,
+ validator: (value) => [URL_MERGE_PARAMS_STRATEGY, URL_SET_PARAMS_STRATEGY].includes(value),
+ },
},
watch: {
query: {
@@ -29,7 +38,11 @@ export default {
},
methods: {
updateQuery(newQuery) {
- historyPushState(mergeUrlParams(newQuery, window.location.href, { spreadArrays: true }));
+ const url =
+ this.urlParamsUpdateStrategy === URL_SET_PARAMS_STRATEGY
+ ? setUrlParams(this.query, window.location.href, true)
+ : mergeUrlParams(newQuery, window.location.href, { spreadArrays: true });
+ historyPushState(url);
},
},
render() {
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index c1e618620d8..6552a874c3a 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -5,29 +5,29 @@
Sample configuration:
- <user-avatar-image
+ <user-avatar
lazy
:img-src="userAvatarSrc"
:img-alt="tooltipText"
:tooltip-text="tooltipText"
tooltip-placement="top"
+ :size="24"
/>
*/
+import { GlTooltip, GlAvatar } from '@gitlab/ui';
+import { isObject } from 'lodash';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __ } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import UserAvatarImageNew from './user_avatar_image_new.vue';
-import UserAvatarImageOld from './user_avatar_image_old.vue';
+import { placeholderImage } from '~/lazy_loader';
export default {
name: 'UserAvatarImage',
components: {
- UserAvatarImageNew,
- UserAvatarImageOld,
+ GlTooltip,
+ GlAvatar,
},
- mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
@@ -51,8 +51,7 @@ export default {
},
size: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
tooltipText: {
type: String,
@@ -64,22 +63,52 @@ export default {
required: false,
default: 'top',
},
- enforceGlAvatar: {
- type: Boolean,
- required: false,
+ },
+ computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside user avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ // Only adds the width to the URL if its not a base64 data image
+ if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
+ baseSrc += `?width=${this.maximumSize}`;
+ return baseSrc;
+ },
+ maximumSize() {
+ if (isObject(this.size)) {
+ return Math.max(...Object.values(this.size));
+ }
+
+ return this.size;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
},
},
};
</script>
<template>
- <user-avatar-image-new
- v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
- v-bind="$props"
- >
- <slot></slot>
- </user-avatar-image-new>
- <user-avatar-image-old v-else v-bind="$props">
- <slot></slot>
- </user-avatar-image-old>
+ <span ref="userAvatar">
+ <gl-avatar
+ :class="{
+ lazy: lazy,
+ [cssClasses]: true,
+ }"
+ :src="resultantSrcAttribute"
+ :data-src="sanitizedSource"
+ :size="size"
+ :alt="imgAlt"
+ />
+
+ <gl-tooltip
+ v-if="tooltipText || $scopedSlots.default"
+ :target="() => $refs.userAvatar"
+ :placement="tooltipPlacement"
+ boundary="window"
+ >
+ <slot>{{ tooltipText }}</slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
deleted file mode 100644
index 6bd66981860..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_new.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar that
- does not need to link to the user's profile. The image and an optional
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar
- lazy
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
- />
-
- */
-
-import { GlTooltip, GlAvatar } from '@gitlab/ui';
-import { isObject } from 'lodash';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { __ } from '~/locale';
-import { placeholderImage } from '~/lazy_loader';
-
-export default {
- name: 'UserAvatarImageNew',
- components: {
- GlTooltip,
- GlAvatar,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- imgSrc: {
- type: String,
- required: false,
- default: defaultAvatarUrl,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: __('user avatar'),
- },
- size: {
- type: [Number, Object],
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- },
- computed: {
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- sanitizedSource() {
- let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- // Only adds the width to the URL if its not a base64 data image
- if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.maximumSize}`;
- return baseSrc;
- },
- maximumSize() {
- if (isObject(this.size)) {
- return Math.max(...Object.values(this.size));
- }
-
- return this.size;
- },
- resultantSrcAttribute() {
- return this.lazy ? placeholderImage : this.sanitizedSource;
- },
- },
-};
-</script>
-
-<template>
- <span ref="userAvatar">
- <gl-avatar
- :class="{
- lazy: lazy,
- [cssClasses]: true,
- }"
- :src="resultantSrcAttribute"
- :data-src="sanitizedSource"
- :size="size"
- :alt="imgAlt"
- />
-
- <gl-tooltip
- v-if="
- tooltipText ||
- $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
- "
- :target="() => $refs.userAvatar"
- :placement="tooltipPlacement"
- boundary="window"
- >
- <slot>{{ tooltipText }}</slot>
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
deleted file mode 100644
index 6e8c200d5c3..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image_old.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar that
- does not need to link to the user's profile. The image and an optional
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-image
- lazy
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :tooltip-text="tooltipText"
- tooltip-placement="top"
- />
-
- */
-
-import { GlTooltip } from '@gitlab/ui';
-import defaultAvatarUrl from 'images/no_avatar.png';
-import { __ } from '~/locale';
-import { placeholderImage } from '~/lazy_loader';
-
-export default {
- name: 'UserAvatarImageOld',
- components: {
- GlTooltip,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- imgSrc: {
- type: String,
- required: false,
- default: defaultAvatarUrl,
- },
- cssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: __('user avatar'),
- },
- size: {
- type: Number,
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- },
- computed: {
- // API response sends null when gravatar is disabled and
- // we provide an empty string when we use it inside user avatar link.
- // In both cases we should render the defaultAvatarUrl
- sanitizedSource() {
- let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
- // Only adds the width to the URL if its not a base64 data image
- if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?'))
- baseSrc += `?width=${this.size}`;
- return baseSrc;
- },
- resultantSrcAttribute() {
- return this.lazy ? placeholderImage : this.sanitizedSource;
- },
- avatarSizeClass() {
- return `s${this.size}`;
- },
- },
-};
-</script>
-
-<template>
- <span>
- <img
- ref="userAvatarImage"
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true,
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- class="avatar"
- />
- <gl-tooltip
- v-if="
- tooltipText ||
- $slots.default /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
- "
- :target="() => $refs.userAvatarImage"
- :placement="tooltipPlacement"
- boundary="window"
- >
- <slot>{{ tooltipText }}</slot>
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index f80abed4d69..1a81da3eb0d 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -9,7 +9,7 @@
:link-href="userProfileUrl"
:img-src="userAvatarSrc"
:img-alt="tooltipText"
- :img-size="20"
+ :img-size="32"
:tooltip-text="tooltipText"
:tooltip-placement="top"
:username="username"
@@ -17,17 +17,18 @@
*/
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import UserAvatarLinkNew from './user_avatar_link_new.vue';
-import UserAvatarLinkOld from './user_avatar_link_old.vue';
+import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
+import UserAvatarImage from './user_avatar_image.vue';
export default {
- name: 'UserAvatarLink',
+ name: 'UserAvatarLinkNew',
components: {
- UserAvatarLinkNew,
- UserAvatarLinkOld,
+ UserAvatarImage,
+ GlAvatarLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
lazy: {
type: Boolean,
@@ -56,8 +57,7 @@ export default {
},
imgSize: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
tooltipText: {
type: String,
@@ -74,29 +74,43 @@ export default {
required: false,
default: '',
},
- enforceGlAvatar: {
- type: Boolean,
- required: false,
+ },
+ computed: {
+ shouldShowUsername() {
+ return this.username.length > 0;
+ },
+ avatarTooltipText() {
+ return this.shouldShowUsername ? '' : this.tooltipText;
},
},
};
</script>
<template>
- <user-avatar-link-new
- v-if="glFeatures.glAvatarForAllUserAvatars || enforceGlAvatar"
- v-bind="$props"
- >
- <slot></slot>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </user-avatar-link-new>
+ <gl-avatar-link :href="linkHref" class="user-avatar-link">
+ <user-avatar-image
+ :img-src="imgSrc"
+ :img-alt="imgAlt"
+ :css-classes="imgCssClasses"
+ :size="imgSize"
+ :tooltip-text="avatarTooltipText"
+ :tooltip-placement="tooltipPlacement"
+ :lazy="lazy"
+ >
+ <slot></slot>
+ </user-avatar-image>
+
+ <span
+ v-if="shouldShowUsername"
+ v-gl-tooltip
+ :title="tooltipText"
+ :tooltip-placement="tooltipPlacement"
+ class="gl-ml-3"
+ data-testid="user-avatar-link-username"
+ >
+ {{ username }}
+ </span>
- <user-avatar-link-old v-else v-bind="$props">
- <slot></slot>
- <template #avatar-badge>
- <slot name="avatar-badge"></slot>
- </template>
- </user-avatar-link-old>
+ <slot name="avatar-badge"></slot>
+ </gl-avatar-link>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
deleted file mode 100644
index 83551c689c4..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_new.vue
+++ /dev/null
@@ -1,122 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar wrapped in
- a clickable link (likely to the user's profile). The link, image, and
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-link
- :link-href="userProfileUrl"
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :img-size="20"
- :tooltip-text="tooltipText"
- :tooltip-placement="top"
- :username="username"
- />
-
-*/
-
-import { GlAvatarLink, GlTooltipDirective } from '@gitlab/ui';
-import UserAvatarImage from './user_avatar_image.vue';
-
-export default {
- name: 'UserAvatarLinkNew',
- components: {
- UserAvatarImage,
- GlAvatarLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- linkHref: {
- type: String,
- required: false,
- default: '',
- },
- imgSrc: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: '',
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgSize: {
- type: [Number, Object],
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- enforceGlAvatar: {
- type: Boolean,
- required: false,
- },
- },
- computed: {
- shouldShowUsername() {
- return this.username.length > 0;
- },
- avatarTooltipText() {
- return this.shouldShowUsername ? '' : this.tooltipText;
- },
- },
-};
-</script>
-
-<template>
- <gl-avatar-link :href="linkHref" class="user-avatar-link">
- <user-avatar-image
- :img-src="imgSrc"
- :img-alt="imgAlt"
- :css-classes="imgCssClasses"
- :size="imgSize"
- :tooltip-text="avatarTooltipText"
- :tooltip-placement="tooltipPlacement"
- :lazy="lazy"
- :enforce-gl-avatar="enforceGlAvatar"
- >
- <slot></slot>
- </user-avatar-image>
-
- <span
- v-if="shouldShowUsername"
- v-gl-tooltip
- :title="tooltipText"
- :tooltip-placement="tooltipPlacement"
- class="gl-ml-3"
- data-testid="user-avatar-link-username"
- >
- {{ username }}
- </span>
-
- <slot name="avatar-badge"></slot>
- </gl-avatar-link>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue
deleted file mode 100644
index c2e46e61e1b..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link_old.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-/* This is a re-usable vue component for rendering a user avatar wrapped in
- a clickable link (likely to the user's profile). The link, image, and
- tooltip can be configured by props passed to this component.
-
- Sample configuration:
-
- <user-avatar-link
- :link-href="userProfileUrl"
- :img-src="userAvatarSrc"
- :img-alt="tooltipText"
- :img-size="20"
- :tooltip-text="tooltipText"
- :tooltip-placement="top"
- :username="username"
- />
-
-*/
-
-import { GlLink, GlTooltipDirective } from '@gitlab/ui';
-import UserAvatarImage from './user_avatar_image.vue';
-
-export default {
- name: 'UserAvatarLinkOld',
- components: {
- GlLink,
- UserAvatarImage,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- lazy: {
- type: Boolean,
- required: false,
- default: false,
- },
- linkHref: {
- type: String,
- required: false,
- default: '',
- },
- imgSrc: {
- type: String,
- required: false,
- default: '',
- },
- imgAlt: {
- type: String,
- required: false,
- default: '',
- },
- imgCssClasses: {
- type: String,
- required: false,
- default: '',
- },
- imgSize: {
- type: Number,
- required: false,
- default: 20,
- },
- tooltipText: {
- type: String,
- required: false,
- default: '',
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'top',
- },
- username: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- shouldShowUsername() {
- return this.username.length > 0;
- },
- avatarTooltipText() {
- return this.shouldShowUsername ? '' : this.tooltipText;
- },
- },
-};
-</script>
-
-<template>
- <span>
- <gl-link :href="linkHref" class="user-avatar-link">
- <user-avatar-image
- :img-src="imgSrc"
- :img-alt="imgAlt"
- :css-classes="imgCssClasses"
- :size="imgSize"
- :tooltip-text="avatarTooltipText"
- :tooltip-placement="tooltipPlacement"
- :lazy="lazy"
- >
- <slot></slot>
- </user-avatar-image>
-
- <span
- v-if="shouldShowUsername"
- v-gl-tooltip
- :title="tooltipText"
- :tooltip-placement="tooltipPlacement"
- data-testid="user-avatar-link-username"
- >
- {{ username }}
- </span>
- <slot name="avatar-badge"></slot>
- </gl-link>
- </span>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
index 9da298ad705..231f5ff3d1f 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
import UserAvatarLink from './user_avatar_link.vue';
@@ -9,7 +8,6 @@ export default {
UserAvatarLink,
GlButton,
},
- mixins: [glFeatureFlagMixin()],
props: {
items: {
type: Array,
@@ -22,8 +20,7 @@ export default {
},
imgSize: {
type: [Number, Object],
- required: false,
- default: 20,
+ required: true,
},
emptyText: {
type: String,
@@ -59,9 +56,6 @@ export default {
return sprintf(__('%{count} more'), { count });
},
- imgCssClasses() {
- return this.glFeatures.glAvatarForAllUserAvatars ? 'gl-mr-3' : '';
- },
},
methods: {
expand() {
@@ -85,7 +79,7 @@ export default {
:img-alt="item.name"
:tooltip-text="item.name"
:img-size="imgSize"
- :img-css-classes="imgCssClasses"
+ img-css-classes="gl-mr-3"
/>
<template v-if="hasBreakpoint">
<gl-button v-if="hasHiddenItems" variant="link" @click="expand">
diff --git a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
index 4b39a8e45bb..80c1fcbacfa 100644
--- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue
@@ -10,7 +10,7 @@ import {
GlAvatarLabeled,
} from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
import { isUserBusy } from '~/set_status_modal/utils';
import Tracking from '~/tracking';
@@ -83,6 +83,8 @@ export default {
return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`;
} else if (this.user.status.message_html) {
return this.user.status.message_html;
+ } else if (this.user.status.emoji) {
+ return glEmojiTag(this.user.status.emoji);
}
return '';
@@ -139,8 +141,9 @@ export default {
await followUser(this.user.id);
this.$emit('follow');
} catch (error) {
- createFlash({
- message: I18N_ERROR_FOLLOW,
+ const message = error.response?.data?.message || I18N_ERROR_FOLLOW;
+ createAlert({
+ message,
error,
captureError: true,
});
@@ -159,7 +162,7 @@ export default {
await unfollowUser(this.user.id);
this.$emit('unfollow');
} catch (error) {
- createFlash({
+ createAlert({
message: I18N_ERROR_UNFOLLOW,
error,
captureError: true,
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 3180bd0d283..86a99b8f0ed 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -103,6 +103,7 @@ export default {
return {
iid: this.iid,
fullPath: this.fullPath,
+ getStatus: true,
};
},
update(data) {
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index b6d69faebb5..a851f84ed2f 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -93,3 +93,6 @@ export const confidentialityInfoText = (workspaceType, issuableType) =>
: __('at least the Reporter role'),
},
);
+
+export const EDITING_MODE_MARKDOWN_FIELD = 'markdownField';
+export const EDITING_MODE_CONTENT_EDITOR = 'contentEditor';
diff --git a/app/assets/javascripts/vue_shared/directives/safe_html.js b/app/assets/javascripts/vue_shared/directives/safe_html.js
new file mode 100644
index 00000000000..450c7fc1bc5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/safe_html.js
@@ -0,0 +1,25 @@
+import { sanitize } from '~/lib/dompurify';
+
+// Mitigate against future dompurify mXSS bypasses by
+// avoiding additional serialize/parse round trip.
+// See https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1782
+// and https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2127
+// for more details.
+const DEFAULT_CONFIG = {
+ RETURN_DOM_FRAGMENT: true,
+};
+
+const transform = (el, binding) => {
+ if (binding.oldValue !== binding.value) {
+ const config = { ...DEFAULT_CONFIG, ...(binding.arg ?? {}) };
+
+ el.textContent = '';
+
+ el.appendChild(sanitize(binding.value, config));
+ }
+};
+
+export default {
+ bind: transform,
+ update: transform,
+};
diff --git a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
index 232749a2d01..624ae7027d5 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue
@@ -1,11 +1,13 @@
<script>
import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue';
import LegacyContainer from './components/legacy_container.vue';
import WelcomePage from './components/welcome.vue';
export default {
components: {
+ NewTopLevelGroupAlert,
GlBreadcrumb,
GlIcon,
WelcomePage,
@@ -79,6 +81,14 @@ export default {
shouldVerify() {
return this.verificationRequired && !this.verificationCompleted;
},
+
+ showNewTopLevelGroupAlert() {
+ if (this.activePanel.detailProps === undefined) {
+ return false;
+ }
+
+ return this.activePanel.detailProps.parentGroupName === '';
+ },
},
created() {
@@ -130,6 +140,7 @@ export default {
<slot name="extra-description"></slot>
</div>
<div class="col-lg-9">
+ <new-top-level-group-alert v-if="showNewTopLevelGroupAlert" />
<gl-breadcrumb v-if="breadcrumbs" :items="breadcrumbs" />
<legacy-container :key="activePanel.name" :selector="activePanel.selector" />
</div>
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
index e0669b3ed27..a4fb30a03a1 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
@@ -1,6 +1,6 @@
<script>
import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
@@ -67,7 +67,7 @@ export default {
},
methods: {
showError(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.apiError,
captureError: true,
error,
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index f6d85599dba..0e1975e1c09 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import ReportSection from '~/reports/components/report_section.vue';
import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
@@ -160,7 +160,7 @@ export default {
this.fetchCounts();
},
showError(error) {
- createFlash({
+ createAlert({
message: this.$options.i18n.apiError,
captureError: true,
error,
diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue
new file mode 100644
index 00000000000..5ec16d4ba15
--- /dev/null
+++ b/app/assets/javascripts/webhooks/components/form_url_app.vue
@@ -0,0 +1,134 @@
+<script>
+import { isEmpty } from 'lodash';
+import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+
+import FormUrlMaskItem from './form_url_mask_item.vue';
+
+export default {
+ components: {
+ FormUrlMaskItem,
+ GlFormGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlLink,
+ },
+ props: {
+ initialUrl: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ initialUrlVariables: {
+ type: Array,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ maskEnabled: !isEmpty(this.initialUrlVariables),
+ url: this.initialUrl,
+ items: isEmpty(this.initialUrlVariables) ? [{}] : this.initialUrlVariables,
+ };
+ },
+ computed: {
+ maskedUrl() {
+ if (!this.url) {
+ return null;
+ }
+
+ let maskedUrl = this.url;
+
+ this.items.forEach(({ key, value }) => {
+ if (!key || !value) {
+ return;
+ }
+
+ const replacementExpression = new RegExp(value, 'g');
+ maskedUrl = maskedUrl.replace(replacementExpression, `{${key}}`);
+ });
+
+ return maskedUrl;
+ },
+ },
+ methods: {
+ onItemInput({ index, key, value }) {
+ this.$set(this.items, index, { key, value });
+ },
+ addItem() {
+ this.items.push({});
+ },
+ removeItem(index) {
+ this.items.splice(index, 1);
+ },
+ },
+ i18n: {
+ addItem: s__('Webhooks|+ Mask another portion of URL'),
+ radioFullUrlText: s__('Webhooks|Show full URL'),
+ radioMaskUrlText: s__('Webhooks|Mask portions of URL'),
+ radioMaskUrlHelp: s__('Webhooks|Do not show sensitive data such as tokens in the UI.'),
+ urlDescription: s__(
+ 'Webhooks|URL must be percent-encoded if it contains one or more special characters.',
+ ),
+ urlLabel: __('URL'),
+ urlPlaceholder: 'http://example.com/trigger-ci.json',
+ urlPreview: s__('Webhooks|URL preview'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-form-group
+ :label="$options.i18n.urlLabel"
+ label-for="webhook-url"
+ :description="$options.i18n.urlDescription"
+ >
+ <gl-form-input
+ id="webhook-url"
+ v-model="url"
+ name="hook[url]"
+ :placeholder="$options.i18n.urlPlaceholder"
+ data-testid="form-url"
+ />
+ </gl-form-group>
+ <div class="gl-mt-5">
+ <gl-form-radio-group v-model="maskEnabled">
+ <gl-form-radio :value="false">{{ $options.i18n.radioFullUrlText }}</gl-form-radio>
+ <gl-form-radio :value="true"
+ >{{ $options.i18n.radioMaskUrlText }}
+ <template #help>
+ {{ $options.i18n.radioMaskUrlHelp }}
+ </template>
+ </gl-form-radio>
+ </gl-form-radio-group>
+
+ <div v-if="maskEnabled" class="gl-ml-6" data-testid="url-mask-section">
+ <form-url-mask-item
+ v-for="({ key, value }, index) in items"
+ :key="index"
+ :index="index"
+ :item-key="key"
+ :item-value="value"
+ @input="onItemInput"
+ @remove="removeItem"
+ />
+ <div class="gl-mb-5">
+ <gl-link @click="addItem">{{ $options.i18n.addItem }}</gl-link>
+ </div>
+
+ <gl-form-group :label="$options.i18n.urlPreview" label-for="webhook-url-preview">
+ <gl-form-input
+ id="webhook-url-preview"
+ :value="maskedUrl"
+ readonly
+ name="hook[url]"
+ data-testid="form-url-preview"
+ />
+ </gl-form-group>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/webhooks/components/form_url_mask_item.vue b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
new file mode 100644
index 00000000000..3b75f9b6c0d
--- /dev/null
+++ b/app/assets/javascripts/webhooks/components/form_url_mask_item.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ index: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ itemKey: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ itemValue: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ keyInputId() {
+ return this.inputId('key');
+ },
+ valueInputId() {
+ return this.inputId('value');
+ },
+ },
+ methods: {
+ inputId(type) {
+ return `webhook-url-mask-item-${type}-${this.index}`;
+ },
+ inputName(type) {
+ return `hook[url_variables][][${type}]`;
+ },
+ onKeyInput(key) {
+ this.$emit('input', { index: this.index, key, value: this.itemValue });
+ },
+ onValueInput(value) {
+ this.$emit('input', { index: this.index, key: this.itemKey, value });
+ },
+ onRemoveClick() {
+ this.$emit('remove', this.index);
+ },
+ },
+ i18n: {
+ keyLabel: s__('Webhooks|How it looks in the UI'),
+ valueLabel: s__('Webhooks|Sensitive portion of URL'),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-flex-end gl-gap-3 gl-mb-3">
+ <gl-form-group
+ :label="$options.i18n.valueLabel"
+ :label-for="valueInputId"
+ class="gl-flex-grow-1 gl-mb-0"
+ data-testid="mask-item-value"
+ >
+ <gl-form-input
+ :id="valueInputId"
+ :name="inputName('value')"
+ :value="itemValue"
+ @input="onValueInput"
+ />
+ </gl-form-group>
+ <gl-form-group
+ :label="$options.i18n.keyLabel"
+ :label-for="keyInputId"
+ class="gl-flex-grow-1 gl-mb-0"
+ data-testid="mask-item-key"
+ >
+ <gl-form-input
+ :id="keyInputId"
+ :name="inputName('key')"
+ :value="itemKey"
+ @input="onKeyInput"
+ />
+ </gl-form-group>
+ <gl-button icon="remove" :aria-label="__('Remove')" @click="onRemoveClick" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/webhooks/index.js b/app/assets/javascripts/webhooks/index.js
new file mode 100644
index 00000000000..1b2b33e44c1
--- /dev/null
+++ b/app/assets/javascripts/webhooks/index.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import FormUrlApp from './components/form_url_app.vue';
+
+export default () => {
+ const el = document.querySelector('.js-vue-webhook-form');
+
+ if (!el) {
+ return null;
+ }
+
+ const { url: initialUrl, urlVariables } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'WebhookFormRoot',
+ render(createElement) {
+ return createElement(FormUrlApp, {
+ props: {
+ initialUrl,
+ initialUrlVariables: urlVariables ? JSON.parse(urlVariables) : undefined,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4585426edaa..4d6a27f61ac 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -10,7 +10,7 @@ import {
GlDropdownDivider,
GlIntersectionObserver,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, uniqueId } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
@@ -126,6 +126,9 @@ export default {
},
},
computed: {
+ assigneesTitleId() {
+ return uniqueId('assignees-title-');
+ },
searchUsers() {
return this.users.nodes.map((node) => addClass({ ...node, ...node.user }));
},
@@ -139,9 +142,6 @@ export default {
property: `type_${this.workItemType}`,
};
},
- assigneeListEmpty() {
- return this.assignees.length === 0;
- },
containerClass() {
return !this.isEditing ? 'gl-shadow-none!' : '';
},
@@ -296,12 +296,14 @@ export default {
<template>
<div class="form-row gl-mb-5 work-item-assignees gl-relative gl-flex-nowrap">
<span
+ :id="assigneesTitleId"
class="gl-font-weight-bold col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="assignees-title"
>{{ assigneeText }}</span
>
<gl-token-selector
ref="tokenSelector"
+ :aria-labelledby="assigneesTitleId"
:selected-tokens="localAssignees"
:container-class="containerClass"
:class="{ 'gl-hover-border-gray-200': canUpdate }"
@@ -319,7 +321,7 @@ export default {
>
<template #empty-placeholder>
<div
- class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-300 gl-pr-4 gl-pl-2 gl-top-2"
+ class="add-assignees gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-secondary gl-pr-4 gl-pl-2 gl-top-2"
data-testid="empty-state"
>
<gl-icon name="profile" />
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index c2e4a50fe31..57babe4569d 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -5,6 +5,7 @@ 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 EditedAt from '~/issues/show/components/edited.vue';
import Tracking from '~/tracking';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import workItemQuery from '../graphql/work_item.query.graphql';
@@ -16,6 +17,7 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
components: {
+ EditedAt,
GlButton,
GlFormGroup,
MarkdownField,
@@ -89,6 +91,15 @@ export default {
workItemType() {
return this.workItem?.workItemType?.name;
},
+ lastEditedAt() {
+ return this.workItemDescription?.lastEditedAt;
+ },
+ lastEditedByName() {
+ return this.workItemDescription?.lastEditedBy?.name;
+ },
+ lastEditedByPath() {
+ return this.workItemDescription?.lastEditedBy?.webPath;
+ },
markdownPreviewPath() {
return `${gon.relative_url_root || ''}/${this.fullPath}/preview_markdown?target_type=${
this.workItemType
@@ -228,12 +239,18 @@ export default {
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
- :aria-label="__('Edit')"
+ :aria-label="__('Edit description')"
@click="startEditing"
/>
</div>
<div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
<div v-else v-safe-html="descriptionHtml" class="md gl-mb-5 gl-min-h-8"></div>
+ <edited-at
+ v-if="lastEditedAt"
+ :updated-at="lastEditedAt"
+ :updated-by-name="lastEditedByName"
+ :updated-by-path="lastEditedByPath"
+ />
</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 3d25df9fcb8..af9b8c6101a 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -7,7 +7,10 @@ import {
GlBadge,
GlButton,
GlTooltipDirective,
+ GlEmptyState,
} from '@gitlab/ui';
+import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
+import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
@@ -20,11 +23,14 @@ import {
WIDGET_TYPE_WEIGHT,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_VIEWED_STORAGE_KEY,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_ITERATION,
} from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
+import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
@@ -35,6 +41,7 @@ import WorkItemDescription from './work_item_description.vue';
import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
+import WorkItemMilestone from './work_item_milestone.vue';
import WorkItemInformation from './work_item_information.vue';
export default {
@@ -49,6 +56,7 @@ export default {
GlLoadingIcon,
GlSkeletonLoader,
GlIcon,
+ GlEmptyState,
WorkItemAssignees,
WorkItemActions,
WorkItemDescription,
@@ -60,6 +68,8 @@ export default {
WorkItemInformation,
LocalStorageSync,
WorkItemTypeIcon,
+ WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+ WorkItemMilestone,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -82,6 +92,7 @@ export default {
data() {
return {
error: undefined,
+ updateError: undefined,
workItem: {},
showInfoBanner: true,
updateInProgress: false,
@@ -100,9 +111,10 @@ export default {
},
error() {
this.error = this.$options.i18n.fetchError;
+ document.title = s__('404|Not found');
},
result() {
- if (!this.isModal) {
+ if (!this.isModal && this.workItem.project) {
const path = this.workItem.project?.fullPath
? ` · ${this.workItem.project.fullPath}`
: '';
@@ -127,7 +139,18 @@ export default {
};
},
skip() {
- return !this.workItemDueDate;
+ return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
+ },
+ },
+ {
+ document: workItemAssigneesSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ skip() {
+ return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
},
],
@@ -152,37 +175,44 @@ export default {
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
+ parentWorkItem() {
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
+ },
+ parentWorkItemConfidentiality() {
+ return this.parentWorkItem?.confidential;
+ },
+ parentUrl() {
+ return `../../issues/${this.parentWorkItem?.iid}`;
+ },
+ workItemIconName() {
+ return this.workItem?.workItemType?.iconName;
+ },
+ noAccessSvgPath() {
+ return `data:image/svg+xml;utf8,${encodeURIComponent(noAccessSvg)}`;
+ },
hasDescriptionWidget() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_DESCRIPTION);
+ return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
},
workItemAssignees() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_ASSIGNEES);
+ return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
workItemLabels() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.isWidgetPresent(WIDGET_TYPE_LABELS);
},
workItemDueDate() {
- return this.workItem?.widgets?.find(
- (widget) => widget.type === WIDGET_TYPE_START_AND_DUE_DATE,
- );
+ return this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE);
},
workItemWeight() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_WEIGHT);
+ return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
workItemHierarchy() {
- return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY);
+ return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
},
- parentWorkItem() {
- return this.workItemHierarchy?.parent;
+ workItemIteration() {
+ return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
- parentWorkItemConfidentiality() {
- return this.parentWorkItem?.confidential;
- },
- parentUrl() {
- return `../../issues/${this.parentWorkItem?.iid}`;
- },
- workItemIconName() {
- return this.workItem?.workItemType?.iconName;
+ workItemMilestone() {
+ return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
},
},
beforeDestroy() {
@@ -191,6 +221,9 @@ export default {
this.dismissBanner();
},
methods: {
+ isWidgetPresent(type) {
+ return this.workItem?.widgets?.find((widget) => widget.type === type);
+ },
dismissBanner() {
this.showInfoBanner = false;
},
@@ -236,7 +269,7 @@ export default {
},
)
.catch((error) => {
- this.error = error.message;
+ this.updateError = error.message;
})
.finally(() => {
this.updateInProgress = false;
@@ -249,8 +282,13 @@ export default {
<template>
<section class="gl-pt-5">
- <gl-alert v-if="error" class="gl-mb-3" variant="danger" @dismiss="error = undefined">
- {{ error }}
+ <gl-alert
+ v-if="updateError"
+ class="gl-mb-3"
+ variant="danger"
+ @dismiss="updateError = undefined"
+ >
+ {{ updateError }}
</gl-alert>
<div v-if="workItemLoading" class="gl-max-w-26 gl-py-5">
@@ -289,7 +327,7 @@ export default {
</li>
</ul>
<work-item-type-icon
- v-else
+ v-else-if="!error"
:work-item-icon-name="workItemIconName"
:work-item-type="workItemType && workItemType.toUpperCase()"
show-text
@@ -316,7 +354,7 @@ export default {
:is-parent-confidential="parentWorkItemConfidentiality"
@deleteWorkItem="$emit('deleteWorkItem', workItemType)"
@toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="error = $event"
+ @error="updateError = $event"
/>
<gl-button
v-if="isModal"
@@ -332,24 +370,25 @@ export default {
:storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
>
<work-item-information
- v-if="showInfoBanner"
+ v-if="showInfoBanner && !error"
:show-info-banner="showInfoBanner"
@work-item-banner-dismissed="dismissBanner"
/>
</local-storage-sync>
<work-item-title
+ v-if="workItem.title"
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
- @error="error = $event"
+ @error="updateError = $event"
/>
<work-item-state
:work-item="workItem"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
- @error="error = $event"
+ @error="updateError = $event"
/>
<work-item-assignees
v-if="workItemAssignees"
@@ -360,24 +399,33 @@ export default {
:work-item-type="workItemType"
:can-invite-members="workItemAssignees.canInviteMembers"
:full-path="fullPath"
- @error="error = $event"
+ @error="updateError = $event"
+ />
+ <work-item-labels
+ v-if="workItemLabels"
+ :work-item-id="workItem.id"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
+ <work-item-due-date
+ v-if="workItemDueDate"
+ :can-update="canUpdate"
+ :due-date="workItemDueDate.dueDate"
+ :start-date="workItemDueDate.startDate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
/>
<template v-if="workItemsMvc2Enabled">
- <work-item-labels
- v-if="workItemLabels"
+ <work-item-milestone
+ v-if="workItemMilestone"
:work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.nodes[0]"
+ :work-item-type="workItemType"
:can-update="canUpdate"
:full-path="fullPath"
- @error="error = $event"
- />
- <work-item-due-date
- v-if="workItemDueDate"
- :can-update="canUpdate"
- :due-date="workItemDueDate.dueDate"
- :start-date="workItemDueDate.startDate"
- :work-item-id="workItem.id"
- :work-item-type="workItemType"
- @error="error = $event"
+ @error="updateError = $event"
/>
</template>
<work-item-weight
@@ -387,14 +435,31 @@ export default {
:weight="workItemWeight.weight"
:work-item-id="workItem.id"
:work-item-type="workItemType"
- @error="error = $event"
+ @error="updateError = $event"
/>
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-iteration
+ v-if="workItemIteration"
+ class="gl-mb-5"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
+ </template>
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
:full-path="fullPath"
class="gl-pt-5"
- @error="error = $event"
+ @error="updateError = $event"
+ />
+ <gl-empty-state
+ v-if="error"
+ :title="$options.i18n.fetchErrorTitle"
+ :description="error"
+ :svg-path="noAccessSvgPath"
/>
</template>
</section>
diff --git a/app/assets/javascripts/work_items/components/work_item_due_date.vue b/app/assets/javascripts/work_items/components/work_item_due_date.vue
index 05f8fa8f5e1..eae11c2bb2f 100644
--- a/app/assets/javascripts/work_items/components/work_item_due_date.vue
+++ b/app/assets/javascripts/work_items/components/work_item_due_date.vue
@@ -198,7 +198,7 @@ export default {
label-cols="3"
label-cols-lg="2"
>
- <span v-if="isReadonlyWithNoDates" class="gl-text-gray-400 gl-ml-4">
+ <span v-if="isReadonlyWithNoDates" class="gl-text-secondary gl-ml-4">
{{ $options.i18n.none }}
</span>
<div v-else class="gl-display-flex gl-flex-wrap gl-gap-5">
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index b8b5198be57..05077862690 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -1,16 +1,22 @@
<script>
import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import workItemQuery from '../graphql/work_item.query.graphql';
-import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_LABELS } from '../constants';
+import {
+ i18n,
+ I18N_WORK_ITEM_ERROR_FETCHING_LABELS,
+ TRACKING_CATEGORY_SHOW,
+ WIDGET_TYPE_LABELS,
+} from '../constants';
function isTokenSelectorElement(el) {
return el?.classList.contains('gl-label-close') || el?.classList.contains('dropdown-item');
@@ -52,6 +58,8 @@ export default {
localLabels: [],
searchKey: '',
searchLabels: [],
+ addLabelIds: [],
+ removeLabelIds: [],
};
},
apollo: {
@@ -68,13 +76,21 @@ export default {
error() {
this.$emit('error', i18n.fetchError);
},
+ subscribeToMore: {
+ document: workItemLabelsSubscription,
+ variables() {
+ return {
+ issuableId: this.workItemId,
+ };
+ },
+ },
},
searchLabels: {
query: labelSearchQuery,
variables() {
return {
fullPath: this.fullPath,
- search: this.searchKey,
+ searchTerm: this.searchKey,
};
},
skip() {
@@ -84,11 +100,14 @@ export default {
return data.workspace?.labels?.nodes.map((node) => addClass({ ...node, ...node.label }));
},
error() {
- this.$emit('error', i18n.fetchError);
+ this.$emit('error', I18N_WORK_ITEM_ERROR_FETCHING_LABELS);
},
},
},
computed: {
+ labelsTitleId() {
+ return uniqueId('labels-title-');
+ },
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@@ -97,10 +116,7 @@ export default {
};
},
allowScopedLabels() {
- return this.labelsWidget.allowScopedLabels;
- },
- listEmpty() {
- return this.labels.length === 0;
+ return this.labelsWidget?.allowsScopedLabels;
},
containerClass() {
return !this.isEditing ? 'gl-shadow-none!' : '';
@@ -109,10 +125,10 @@ export default {
return this.$apollo.queries.searchLabels.loading;
},
labelsWidget() {
- return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
+ return this.workItem?.widgets?.find((widget) => widget.type === WIDGET_TYPE_LABELS);
},
labels() {
- return this.labelsWidget?.nodes || [];
+ return this.labelsWidget?.labels?.nodes || [];
},
},
watch: {
@@ -131,44 +147,74 @@ export default {
},
removeLabel({ id }) {
this.localLabels = this.localLabels.filter((label) => label.id !== id);
+ this.removeLabelIds.push(id);
+ this.setLabels();
},
- setLabels(event) {
+ async setLabels() {
+ if (this.addLabelIds.length === 0 && this.removeLabelIds.length === 0) return;
+
this.searchKey = '';
- if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
this.isEditing = false;
- this.$apollo
- .mutate({
- mutation: localUpdateWorkItemMutation,
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
- labels: this.localLabels,
+ labelsWidget: {
+ addLabelIds: this.addLabelIds,
+ removeLabelIds: this.removeLabelIds,
+ },
},
},
- })
- .catch((e) => {
- this.$emit('error', e);
});
- this.track('updated_labels');
+
+ if (errors.length > 0) {
+ this.throwUpdateError();
+ return;
+ }
+
+ this.addLabelIds = [];
+ this.removeLabelIds = [];
+
+ this.track('updated_labels');
+ } catch {
+ this.throwUpdateError();
+ }
+ },
+ throwUpdateError() {
+ this.$emit('error', i18n.updateError);
+ // If mutation is rejected, we're rolling back to initial state
+ this.localLabels = this.labels.map(addClass);
+ this.addLabelIds = [];
+ this.removeLabelIds = [];
+ },
+ handleBlur(event) {
+ if (isTokenSelectorElement(event.relatedTarget) || !this.isEditing) return;
+ this.setLabels();
},
handleFocus() {
this.isEditing = true;
this.searchStarted = true;
},
async focusTokenSelector(labels) {
- if (this.allowScopedLabels) {
- const newLabel = labels[labels.length - 1];
- const existingLabels = labels.slice(0, labels.length - 1);
-
- const newLabelKey = scopedLabelKey(newLabel);
+ const labelsToAdd = without(labels, ...this.localLabels).map((label) => label.id);
+ const labelsToRemove = without(this.localLabels, ...labels).map((label) => label.id);
- const removeLabelsWithSameScope = existingLabels.filter((label) => {
- const sameKey = newLabelKey === scopedLabelKey(label);
- return !sameKey;
- });
+ if (labelsToAdd.length > 0) {
+ this.addLabelIds.push(...labelsToAdd);
+ }
- this.localLabels = [...removeLabelsWithSameScope, newLabel];
+ if (labelsToRemove.length > 0) {
+ this.removeLabelIds.push(...labelsToRemove);
}
+
+ this.localLabels = labels;
+
this.handleFocus();
await this.$nextTick();
this.$refs.tokenSelector.focusTextInput();
@@ -194,13 +240,15 @@ export default {
<template>
<div class="form-row gl-mb-5 work-item-labels gl-relative gl-flex-nowrap">
<span
+ :id="labelsTitleId"
class="gl-font-weight-bold gl-mt-2 col-lg-2 col-3 gl-pt-2 min-w-fit-content gl-overflow-wrap-break"
data-testid="labels-title"
>{{ __('Labels') }}</span
>
<gl-token-selector
ref="tokenSelector"
- v-model="localLabels"
+ :selected-tokens="localLabels"
+ :aria-labelledby="labelsTitleId"
:container-class="containerClass"
:dropdown-items="searchLabels"
:loading="isLoading"
@@ -210,13 +258,13 @@ export default {
@input="focusTokenSelector"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
- @blur="setLabels"
+ @blur="handleBlur"
@mouseover.native="handleMouseOver"
@mouseout.native="handleMouseOut"
>
<template #empty-placeholder>
<div
- class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-gray-400 gl-pr-4 gl-top-2"
+ class="add-labels gl-min-w-fit-content gl-display-flex gl-align-items-center gl-text-secondary gl-pr-4 gl-top-2"
data-testid="empty-state"
>
<span v-if="canUpdate" class="gl-ml-2">{{ __('Add labels') }}</span>
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 8f31b07b6a3..37aa48be6e5 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
@@ -16,7 +16,13 @@ export default function initWorkItemLinks() {
return;
}
- const { projectPath, wiHasIssueWeightsFeature, iid } = workItemLinksRoot.dataset;
+ const {
+ projectPath,
+ wiHasIssueWeightsFeature,
+ iid,
+ wiHasIterationsFeature,
+ projectNamespace,
+ } = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -31,6 +37,8 @@ export default function initWorkItemLinks() {
iid,
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
+ hasIterationsFeature: wiHasIterationsFeature,
+ projectNamespace,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index 840fd910272..0d3e951de7e 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -5,7 +5,7 @@ import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -59,7 +59,7 @@ export default {
},
},
parentIssue: {
- query: issueConfidentialQuery,
+ query: getIssueDetailsQuery,
variables() {
return {
fullPath: this.projectPath,
@@ -86,6 +86,9 @@ export default {
confidential() {
return this.parentIssue?.confidential || this.workItem?.confidential || false;
},
+ issuableIteration() {
+ return this.parentIssue?.iteration;
+ },
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@@ -257,7 +260,7 @@ export default {
class="gl-display-inline-flex gl-align-items-center gl-line-height-24 gl-ml-3"
data-testid="children-count"
>
- <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-gray-500" />
+ <gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-2 gl-text-secondary" />
{{ childrenCountLabel }}
</span>
</div>
@@ -294,7 +297,7 @@ export default {
<template v-else>
<div v-if="isChildrenEmpty && !isShownAddForm && !error" data-testid="links-empty">
- <p class="gl-mt-3 gl-mb-4">
+ <p class="gl-mb-3">
{{ $options.i18n.emptyStateMessage }}
</p>
</div>
@@ -305,6 +308,7 @@ export default {
:issuable-gid="issuableGid"
:children-ids="childrenIds"
:parent-confidential="confidential"
+ :parent-iteration="issuableIteration"
@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 8b848995d44..a01f4616cab 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
@@ -16,7 +16,7 @@ export default {
GlFormGroup,
GlFormInput,
},
- inject: ['projectPath'],
+ inject: ['projectPath', 'hasIterationsFeature'],
props: {
issuableGid: {
type: String,
@@ -33,6 +33,11 @@ export default {
required: false,
default: false,
},
+ parentIteration: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
apollo: {
workItemTypes: {
@@ -77,6 +82,9 @@ export default {
taskWorkItemType() {
return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
},
+ parentIterationId() {
+ return this.parentIteration?.id;
+ },
},
methods: {
getIdFromGraphQLId,
@@ -133,6 +141,13 @@ export default {
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
+ /**
+ * call update mutation only when there is an iteration associated with the issue
+ */
+ // TODO: setting the iteration should be moved to the creation mutation once the backend is done
+ if (this.parentIterationId && this.hasIterationsFeature) {
+ this.addIterationToWorkItem(data.workItemCreate.workItem.id);
+ }
}
})
.catch(() => {
@@ -143,6 +158,19 @@ export default {
this.childToCreateTitle = null;
});
},
+ async addIterationToWorkItem(workItemId) {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: workItemId,
+ iterationWidget: {
+ iterationId: this.parentIterationId,
+ },
+ },
+ },
+ });
+ },
},
i18n: {
inputLabel: __('Title'),
@@ -182,7 +210,7 @@ export default {
>
<template #result="{ item }">
<div class="gl-display-flex">
- <div class="gl-text-gray-400 gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
+ <div class="gl-text-secondary gl-mr-4">{{ getIdFromGraphQLId(item.id) }}</div>
<div>{{ item.title }}</div>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
new file mode 100644
index 00000000000..c4a36e36555
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -0,0 +1,248 @@
+<script>
+import {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import { debounce } from 'lodash';
+import Tracking from '~/tracking';
+import { s__ } from '~/locale';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+const noMilestoneId = 'no-milestone-id';
+
+export default {
+ i18n: {
+ MILESTONE: s__('WorkItem|Milestone'),
+ NONE: s__('WorkItem|None'),
+ MILESTONE_PLACEHOLDER: s__('WorkItem|Add to milestone'),
+ NO_MATCHING_RESULTS: s__('WorkItem|No matching results'),
+ NO_MILESTONE: s__('WorkItem|No milestone'),
+ MILESTONE_FETCH_ERROR: s__(
+ 'WorkItem|Something went wrong while fetching milestones. Please try again.',
+ ),
+ },
+ components: {
+ GlFormGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSkeletonLoader,
+ GlSearchBoxByType,
+ GlDropdownText,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemMilestone: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ localMilestone: this.workItemMilestone,
+ searchTerm: '',
+ shouldFetch: false,
+ updateInProgress: false,
+ isFocused: false,
+ milestones: [],
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ emptyPlaceholder() {
+ return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
+ },
+ dropdownText() {
+ return this.localMilestone?.title || this.emptyPlaceholder;
+ },
+ isLoadingMilestones() {
+ return this.$apollo.queries.milestones.loading;
+ },
+ isNoMilestone() {
+ return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id;
+ },
+ dropdownClasses() {
+ return {
+ 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
+ 'is-not-focused': !this.isFocused,
+ };
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ milestones: {
+ query: projectMilestonesQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ title: this.searchTerm,
+ first: 20,
+ };
+ },
+ skip() {
+ return !this.shouldFetch;
+ },
+ update(data) {
+ return data?.workspace?.attributes?.nodes || [];
+ },
+ error() {
+ this.$emit('error', this.i18n.MILESTONE_FETCH_ERROR);
+ },
+ },
+ },
+ methods: {
+ handleMilestoneClick(milestone) {
+ this.localMilestone = milestone;
+ },
+ onDropdownShown() {
+ this.$refs.search.focusInput();
+ this.shouldFetch = true;
+ this.isFocused = true;
+ },
+ onDropdownHide() {
+ this.isFocused = false;
+ this.searchTerm = '';
+ this.shouldFetch = false;
+ this.updateMilestone();
+ },
+ setSearchKey(value) {
+ this.searchTerm = value;
+ },
+ isMilestoneChecked(milestone) {
+ return this.localMilestone?.id === milestone?.id;
+ },
+ updateMilestone() {
+ if (this.workItemMilestone?.id === this.localMilestone?.id) {
+ return;
+ }
+
+ this.track('updated_milestone');
+ this.updateInProgress = true;
+ this.$apollo
+ .mutate({
+ mutation: localUpdateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ milestone: {
+ milestoneId: this.localMilestone?.id,
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('\n'));
+ }
+ })
+ .catch((error) => {
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.updateInProgress = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="work-item-dropdown"
+ :label="$options.i18n.MILESTONE"
+ label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
+ label-cols="3"
+ label-cols-lg="2"
+ >
+ <span
+ v-if="!canUpdate"
+ class="gl-text-secondary gl-ml-4 gl-mt-3 gl-display-inline-block gl-line-height-normal"
+ data-testid="disabled-text"
+ >
+ {{ dropdownText }}
+ </span>
+ <gl-dropdown
+ v-else
+ :toggle-class="dropdownClasses"
+ :text="dropdownText"
+ :loading="updateInProgress"
+ @shown="onDropdownShown"
+ @hide="onDropdownHide"
+ >
+ <template #header>
+ <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" />
+ </template>
+ <gl-dropdown-item
+ data-testid="no-milestone"
+ is-check-item
+ :is-checked="isNoMilestone"
+ @click="handleMilestoneClick({ id: 'no-milestone-id' })"
+ >
+ {{ $options.i18n.NO_MILESTONE }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-text v-if="isLoadingMilestones">
+ <gl-skeleton-loader :height="90">
+ <rect width="380" height="10" x="10" y="15" rx="4" />
+ <rect width="280" height="10" x="10" y="30" rx="4" />
+ <rect width="380" height="10" x="10" y="50" rx="4" />
+ <rect width="280" height="10" x="10" y="65" rx="4" />
+ </gl-skeleton-loader>
+ </gl-dropdown-text>
+ <template v-else-if="milestones.length">
+ <gl-dropdown-item
+ v-for="milestone in milestones"
+ :key="milestone.id"
+ is-check-item
+ :is-checked="isMilestoneChecked(milestone)"
+ @click="handleMilestoneClick(milestone)"
+ >
+ {{ milestone.title }}
+ </gl-dropdown-item>
+ </template>
+ <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>
+ </gl-dropdown>
+ </gl-form-group>
+</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 31e75663055..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
@@ -53,7 +53,7 @@ export default {
v-gl-tooltip.hover="showTooltipOnHover"
:name="iconName"
:title="workItemTooltipTitle"
- class="gl-mr-2 gl-text-gray-500"
+ class="gl-mr-2 gl-text-secondary"
/>
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
</span>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 78219e62d01..7737c535650 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -17,6 +17,9 @@ export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
+export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
+export const WIDGET_TYPE_ITERATION = 'ITERATION';
+
export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
@@ -26,13 +29,19 @@ export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
export const WORK_ITEM_TYPE_ENUM_REQUIREMENTS = 'REQUIREMENTS';
export const i18n = {
- fetchError: s__('WorkItem|Something went wrong when fetching the work item. Please try again.'),
+ fetchErrorTitle: s__('WorkItem|Work item not found'),
+ fetchError: s__(
+ "WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it.",
+ ),
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.',
),
};
+export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
+ 'WorkItem|Something went wrong when fetching labels. Please try again.',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -48,6 +57,10 @@ export const I18N_WORK_ITEM_ARE_YOU_SURE_DELETE = s__(
);
export const I18N_WORK_ITEM_DELETED = s__('WorkItem|%{workItemType} deleted');
+export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
+ 'WorkItem|Something went wrong when fetching iterations. Please try again.',
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
diff --git a/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
new file mode 100644
index 00000000000..6edb6c89f16
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/get_issue_details.query.graphql
@@ -0,0 +1,9 @@
+query issuableDetails($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ issuable: issue(iid: $iid) {
+ id
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
index 36ffba8a540..36779dfe11e 100644
--- a/app/assets/javascripts/work_items/graphql/typedefs.graphql
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
@@ -1,6 +1,6 @@
enum LocalWidgetType {
ASSIGNEES
- LABELS
+ MILESTONE
}
interface LocalWorkItemWidget {
@@ -12,10 +12,9 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
-type LocalWorkItemLabels implements LocalWorkItemWidget {
+type LocalWorkItemMilestone implements LocalWorkItemWidget {
type: LocalWidgetType!
- allowScopedLabels: Boolean!
- nodes: [Label!]
+ nodes: [Milestone!]
}
extend type WorkItem {
@@ -30,17 +29,14 @@ input LocalUserInput {
avatarUrl: String
}
-input LocalLabelInput {
- id: ID!
- title: String!
- color: String
- description: String
+input LocalMilestoneInput {
+ milestoneId: ID!
}
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [LocalUserInput!]
- labels: [LocalLabelInput]
+ milestone: LocalMilestoneInput!
}
type LocalWorkItemPayload {
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index f4c77ed2ec0..bb05c9b2135 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -1,4 +1,3 @@
-#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "ee_else_ce/work_items/graphql/work_item_widgets.fragment.graphql"
fragment WorkItem on WorkItem {
diff --git a/app/assets/javascripts/work_items/graphql/work_item.query.graphql b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
index 276061af193..fa0ab56df75 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.query.graphql
@@ -1,15 +1,16 @@
-#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "./work_item.fragment.graphql"
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
mockWidgets @client {
- ... on LocalWorkItemLabels {
+ ... on LocalWorkItemMilestone {
type
- allowScopedLabels
nodes {
- ...Label
+ id
+ title
+ expired
+ dueDate
}
}
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql
new file mode 100644
index 00000000000..d5b2de8c4c6
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql
@@ -0,0 +1,21 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+subscription issuableAssignees($issuableId: IssuableID!) {
+ issuableAssigneesUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetAssignees {
+ type
+ allowsMultipleAssignees
+ canInviteMembers
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
index 7e045fdf431..d8760f147e1 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_dates.subscription.graphql
@@ -4,6 +4,7 @@ subscription issuableDatesUpdated($issuableId: IssuableID!) {
id
widgets {
... on WorkItemWidgetStartAndDueDate {
+ type
dueDate
startDate
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql
new file mode 100644
index 00000000000..86d936bf4dd
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql
@@ -0,0 +1,19 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+
+subscription workItemLabels($issuableId: IssuableID!) {
+ issuableLabelsUpdated(issuableId: $issuableId) {
+ ... on WorkItem {
+ id
+ widgets {
+ ... on WorkItemWidgetLabels {
+ type
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ }
+ }
+ }
+}
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 3005069f59a..d404cfb10ed 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,8 +1,16 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
type
description
descriptionHtml
+ lastEditedAt
+ lastEditedBy {
+ name
+ webPath
+ }
}
... on WorkItemWidgetAssignees {
type
@@ -14,6 +22,14 @@ fragment WorkItemWidgets on WorkItemWidget {
}
}
}
+ ... on WorkItemWidgetLabels {
+ type
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
... on WorkItemWidgetStartAndDueDate {
type
dueDate
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index bb4c7052238..f872d8c6b12 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,7 +6,13 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, hasIssueWeightsFeature, issuesListPath } = el.dataset;
+ const {
+ fullPath,
+ hasIssueWeightsFeature,
+ issuesListPath,
+ projectNamespace,
+ hasIterationsFeature,
+ } = el.dataset;
return new Vue({
el,
@@ -17,6 +23,8 @@ export const initWorkItemsRoot = () => {
fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
issuesListPath,
+ projectNamespace,
+ hasIterationsFeature: parseBoolean(hasIterationsFeature),
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 3b7257591e2..4908b99e5b0 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -6,7 +6,6 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
import workItemQuery from '../graphql/work_item.query.graphql';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
-import createWorkItemFromTaskMutation from '../graphql/create_work_item_from_task.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import ItemTitle from '../components/item_title.vue';
@@ -29,26 +28,6 @@ export default {
required: false,
default: '',
},
- issueGid: {
- type: String,
- required: false,
- default: '',
- },
- lockVersion: {
- type: Number,
- required: false,
- default: null,
- },
- lineNumberStart: {
- type: String,
- required: false,
- default: null,
- },
- lineNumberEnd: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -136,28 +115,6 @@ export default {
this.error = this.createErrorText;
}
},
- async createWorkItemFromTask() {
- try {
- const { data } = await this.$apollo.mutate({
- mutation: createWorkItemFromTaskMutation,
- variables: {
- input: {
- id: this.issueGid,
- workItemData: {
- lockVersion: this.lockVersion,
- title: this.title,
- lineNumberStart: Number(this.lineNumberStart),
- lineNumberEnd: Number(this.lineNumberEnd),
- workItemTypeId: this.selectedWorkItemType,
- },
- },
- },
- });
- this.$emit('onCreate', data.workItemCreateFromTask.workItem.descriptionHtml);
- } catch {
- this.error = this.createErrorText;
- }
- },
handleTitleInput(title) {
this.title = title;
},
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 9e81e1d4771..21d9db26382 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -1,13 +1,9 @@
@import './pages/branches';
-@import './pages/clusters';
@import './pages/colors';
@import './pages/commits';
-@import './pages/deploy_keys';
@import './pages/detail_page';
-@import './pages/environment_logs';
@import './pages/events';
@import './pages/groups';
-@import './pages/help';
@import './pages/hierarchy';
@import './pages/issuable';
@import './pages/issues';
@@ -21,11 +17,8 @@
@import './pages/pipelines';
@import './pages/profile';
@import './pages/projects';
-@import './pages/prometheus';
@import './pages/registry';
@import './pages/search';
-@import './pages/service_desk';
@import './pages/settings';
@import './pages/storage_quota';
-@import './pages/tree';
@import './pages/users';
diff --git a/app/assets/stylesheets/bootstrap_migration_reset.scss b/app/assets/stylesheets/bootstrap_migration_reset.scss
index ad315c4ada1..fb112a2ee84 100644
--- a/app/assets/stylesheets/bootstrap_migration_reset.scss
+++ b/app/assets/stylesheets/bootstrap_migration_reset.scss
@@ -54,10 +54,6 @@ strong {
font-weight: bold;
}
-a {
- color: $blue-600;
-}
-
hr {
overflow: hidden;
}
diff --git a/app/assets/stylesheets/components/batch_comments/review_bar.scss b/app/assets/stylesheets/components/batch_comments/review_bar.scss
deleted file mode 100644
index 5e1128dc4ce..00000000000
--- a/app/assets/stylesheets/components/batch_comments/review_bar.scss
+++ /dev/null
@@ -1,71 +0,0 @@
-.review-bar-component {
- position: fixed;
- bottom: 0;
- left: 0;
- z-index: $zindex-dropdown-menu;
- display: flex;
- align-items: center;
- width: 100%;
- height: $toggle-sidebar-height;
- padding-left: $contextual-sidebar-width;
- padding-right: $gutter_collapsed_width;
- background: $white;
- border-top: 1px solid $border-color;
- transition: padding $gl-transition-duration-medium;
-
- .page-with-icon-sidebar & {
- padding-left: $contextual-sidebar-collapsed-width;
- }
-
- .right-sidebar-expanded & {
- padding-right: $gutter_width;
- }
-
- @media (max-width: map-get($grid-breakpoints, sm)-1) {
- padding-left: 0;
- padding-right: 0;
- }
-
- .dropdown {
- margin-left: $grid-size;
- }
-}
-
-.review-bar-content {
- max-width: $limited-layout-width;
- padding: 0 $gl-padding;
- width: 100%;
- margin: 0 auto;
-}
-
-.review-preview-item-header {
- display: flex;
- align-items: center;
- width: 100%;
- margin-bottom: 4px;
-
- > .bold {
- display: flex;
- min-width: 0;
- line-height: 16px;
- }
-}
-
-.review-preview-item-footer {
- display: flex;
- align-items: center;
- margin-top: 4px;
-}
-
-.review-preview-item-content {
- width: 100%;
-
- p {
- display: block;
- width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- margin-bottom: 0;
- }
-}
diff --git a/app/assets/stylesheets/components/date_time_picker.scss b/app/assets/stylesheets/components/date_time_picker.scss
deleted file mode 100644
index 21f085cdaf1..00000000000
--- a/app/assets/stylesheets/components/date_time_picker.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.date-time-picker {
- .date-time-picker-menu {
- width: 400px;
- }
-}
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
deleted file mode 100644
index b8bd1000bfd..00000000000
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ /dev/null
@@ -1,193 +0,0 @@
-$design-pin-diameter: 28px;
-$design-pin-diameter-sm: 24px;
-$t-gray-a-16-design-pin: rgba($black, 0.16);
-
-.layout-page.design-detail-layout {
- max-height: 100vh;
-}
-
-.design-detail {
- background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
-
- .with-performance-bar & {
- top: 35px;
- }
-
- .comment-indicator {
- border-radius: 50%;
- }
-
- .comment-indicator,
- .frame .design-note-pin {
- &:active {
- cursor: grabbing;
- }
- }
-}
-
-.design-scaler-wrapper {
- bottom: 0;
- left: 50%;
- transform: translateX(-50%);
-}
-
-.design-checkbox {
- position: absolute;
- top: $gl-padding;
- left: 30px;
-}
-
-.image-notes {
- overflow-y: scroll;
- padding: $gl-padding;
- padding-top: 50px;
- background-color: $white;
- flex-shrink: 0;
- min-width: 400px;
- flex-basis: 28%;
-
- .link-inherit-color {
- &:hover,
- &:active,
- &:focus {
- color: inherit;
- text-decoration: none;
- }
- }
-
- .toggle-comments {
- line-height: 20px;
- border-top: 1px solid $border-color;
-
- &.expanded {
- border-bottom: 1px solid $border-color;
- }
-
- .toggle-comments-button:focus {
- text-decoration: none;
- color: $blue-600;
- }
- }
-
- .design-note-pin {
- margin-left: $gl-padding;
- }
-
- .design-discussion {
- margin: $gl-padding 0;
-
- &::before {
- content: '';
- border-left: 1px solid $gray-100;
- position: absolute;
- left: 28px;
- top: -17px;
- height: 17px;
- }
-
- .design-note {
- padding: $gl-padding;
- list-style: none;
- transition: background $gl-transition-duration-medium $general-hover-transition-curve;
- border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
- border-top-right-radius: $border-radius-default;
-
- a {
- color: inherit;
- }
-
- .note-text a {
- color: $blue-600;
- }
- }
-
- .reply-wrapper {
- padding: $gl-padding;
- }
- }
-
- .reply-wrapper {
- border-top: 1px solid $border-color;
- }
-
- .new-discussion-disclaimer {
- line-height: 20px;
- }
-}
-
-@media (max-width: map-get($grid-breakpoints, lg)) {
- .design-detail {
- overflow-y: scroll;
- }
-
- .image-notes {
- overflow-y: auto;
- min-width: 100%;
- flex-grow: 1;
- flex-basis: auto;
- }
-}
-
-.design-card-header {
- background: transparent;
-}
-
-.design-note-pin {
- display: flex;
- height: $design-pin-diameter;
- width: $design-pin-diameter;
- box-sizing: content-box;
- background-color: $purple-500;
- color: $white;
- font-weight: $gl-font-weight-bold;
- border-radius: 50%;
- z-index: 1;
- padding: 0;
- border: 0;
-
- &.draft {
- background-color: $orange-500;
- }
-
- &.resolved {
- background-color: $gray-500;
- }
-
- &.on-image {
- box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
- border: $white 2px solid;
- will-change: transform, box-shadow, opacity;
- // NOTE: verbose transition property required for Safari
- transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
- transform-origin: 0 0;
- transform: translate(-50%, -50%);
-
- &:hover {
- transform: scale(1.2) translate(-50%, -50%);
- }
-
- &:active {
- box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
- }
-
- &.inactive {
- @include gl-opacity-5;
-
- &:hover {
- @include gl-opacity-10;
- }
- }
- }
-
- &.small {
- position: absolute;
- border: 1px solid $white;
- height: $design-pin-diameter-sm;
- width: $design-pin-diameter-sm;
- }
-
- &.user-avatar {
- top: 25px;
- right: 8px;
- }
-}
diff --git a/app/assets/stylesheets/components/design_management/design_list_item.scss b/app/assets/stylesheets/components/design_management/design_list_item.scss
deleted file mode 100644
index 09af4da37e9..00000000000
--- a/app/assets/stylesheets/components/design_management/design_list_item.scss
+++ /dev/null
@@ -1,19 +0,0 @@
-.design-list-item {
- height: 280px;
- text-decoration: none;
-
- .icon-version-status {
- position: absolute;
- right: 10px;
- top: 10px;
- }
-
- .card-body {
- height: 230px;
- }
-}
-
-// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197
-.design-list-item-new {
- height: 210px;
-}
diff --git a/app/assets/stylesheets/components/feature_highlight.scss b/app/assets/stylesheets/components/feature_highlight.scss
deleted file mode 100644
index 4d301cc5617..00000000000
--- a/app/assets/stylesheets/components/feature_highlight.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.gl-sm-mr-3 {
- @media (min-width: $breakpoint-sm) {
- @include gl-mr-3;
- }
-}
diff --git a/app/assets/stylesheets/components/milestone_combobox.scss b/app/assets/stylesheets/components/milestone_combobox.scss
deleted file mode 100644
index 94d295c324b..00000000000
--- a/app/assets/stylesheets/components/milestone_combobox.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.milestone-combobox {
- .dropdown-menu.show {
- overflow: hidden;
- }
-}
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 3bb889b6ba0..293caf6fc87 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -75,19 +75,6 @@ $item-remove-button-space: 42px;
}
}
-.item-body,
-.card-header {
- .health-label-short {
- max-width: 0;
- }
-}
-
-.card-header {
- .health-label-short {
- display: initial;
- }
-}
-
.item-meta {
flex-basis: 100%;
font-size: $gl-font-size;
@@ -212,11 +199,6 @@ $item-remove-button-space: 42px;
max-width: 90%;
}
- .card-header {
- .health-label-short {
- max-width: 30px;
- }
- }
}
/* Small devices (landscape phones, 768px and up) */
@@ -239,11 +221,6 @@ $item-remove-button-space: 42px;
}
}
- .card-header {
- .health-label-short {
- max-width: 60px;
- }
- }
}
/* Medium devices (desktops, 992px and up) */
@@ -257,12 +234,6 @@ $item-remove-button-space: 42px;
font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small`
}
}
-
- .card-header {
- .health-label-short {
- max-width: 100px;
- }
- }
}
/* Large devices (large desktops, 1200px and up) */
@@ -309,15 +280,3 @@ $item-remove-button-space: 42px;
flex-basis: auto;
}
}
-
-@media only screen and (min-width: 1500px) {
- .card-header {
- .health-label-short {
- display: none;
- }
-
- .health-label-long {
- display: block;
- }
- }
-}
diff --git a/app/assets/stylesheets/components/release_block.scss b/app/assets/stylesheets/components/release_block.scss
deleted file mode 100644
index 7e82d0960d7..00000000000
--- a/app/assets/stylesheets/components/release_block.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.release-block {
- transition: background-color 1s linear;
-}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/components/shortcuts_help.scss
index 9182292ffd3..ea2281538b4 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/components/shortcuts_help.scss
@@ -27,14 +27,3 @@
}
}
}
-
-.documentation {
- padding: 7px;
- font-size: $gl-font-size-large;
-}
-
-.card.links-card {
- a {
- color: $blue-600;
- }
-}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index e977fb92928..07db6b3c147 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -42,7 +42,6 @@
@import 'framework/notes';
@import 'framework/tabs';
@import 'framework/timeline';
-@import 'framework/toggle';
@import 'framework/typography';
@import 'framework/zen';
@import 'framework/wells';
@@ -54,14 +53,11 @@
@import 'framework/emojis';
@import 'framework/icons';
@import 'framework/snippets';
-@import 'framework/memory_graph';
@import 'framework/responsive_tables';
@import 'framework/stacked_progress_bar';
@import 'framework/sortable';
-@import 'framework/ci_variable_list';
@import 'framework/feature_highlight';
@import 'framework/read_more';
-@import 'framework/flex_grid';
@import 'framework/system_messages';
@import 'framework/spinner';
@import 'framework/card';
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f947042ba51..799777977ed 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -66,7 +66,6 @@
}
&.content-component-block {
- padding: 8px 0;
background-color: $body-bg;
}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index b1e5ca50a8b..e69d7b4462d 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -1,9 +1,17 @@
.user-contrib-cell {
+ stroke: $t-gray-a-08;
+
&:hover {
cursor: pointer;
stroke: $black;
}
+ &:focus {
+ @include gl-outline-none;
+ stroke: $white;
+ filter: drop-shadow(1px 0 0.5px $blue-400) drop-shadow(0 1px 0.5px $blue-400) drop-shadow(-1px 0 0.5px $blue-400) drop-shadow(0 -1px 0.5px $blue-400);
+ }
+
// `app/assets/javascripts/pages/users/activity_calendar.js` sets this attribute
@for $i from 1 through length($calendar-activity-colors) {
$color: nth($calendar-activity-colors, $i);
diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss
deleted file mode 100644
index ef4355ad157..00000000000
--- a/app/assets/stylesheets/framework/ci_variable_list.scss
+++ /dev/null
@@ -1,77 +0,0 @@
-.ci-variable-list {
- margin-left: 0;
- margin-bottom: 0;
- padding-left: 0;
- list-style: none;
- clear: both;
-}
-
-.ci-variable-row {
- display: flex;
- align-items: flex-start;
-
- @include media-breakpoint-down(xs) {
- align-items: flex-end;
- }
-
- &:not(:last-child) {
- margin-bottom: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-bottom: 3 * $gl-btn-padding;
- }
- }
-
- &:last-child {
- .ci-variable-body-item:last-child {
- margin-right: $ci-variable-remove-button-width;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- }
- }
-
- .ci-variable-row-remove-button {
- display: none;
- }
-
- @include media-breakpoint-down(xs) {
- .ci-variable-row-body {
- margin-right: $ci-variable-remove-button-width;
- }
- }
- }
-}
-
-.ci-variable-row-body {
- display: flex;
- align-items: flex-start;
- width: 100%;
- padding-bottom: $gl-padding;
-
- @include media-breakpoint-down(xs) {
- display: block;
- }
-}
-
-.ci-variable-body-item {
- flex: 1;
-
- &:not(:last-child) {
- margin-right: $gl-btn-padding;
-
- @include media-breakpoint-down(xs) {
- margin-right: 0;
- margin-bottom: $gl-btn-padding;
- }
- }
-}
-
-.ci-variable-masked-item,
-.ci-variable-protected-item {
- flex: 0 1 auto;
- display: flex;
- align-items: center;
- padding-top: 5px;
- padding-bottom: 5px;
-}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 8d1fb5eb55f..f7cd5d7e183 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -43,7 +43,7 @@
z-index: 120;
&.is-sidebar-moved {
- --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 28px});
+ --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 24px});
}
.with-system-header & {
@@ -578,78 +578,6 @@ table.code {
}
}
-// Merge request diff grid layout
-.diff-grid {
- .diff-td {
- // By default min-width is auto with 1fr which causes some overflow problems
- // https://gitlab.com/gitlab-org/gitlab/-/issues/296222
- min-width: 0;
- }
-
- .diff-grid-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
-
- &.diff-grid-row-full {
- grid-template-columns: 1fr;
- }
- }
-
- .diff-grid-left,
- .diff-grid-right {
- display: grid;
- // Zero width column is a placeholder for the EE inline code quality diff
- // see ee/.../diffs.scss for more details
- grid-template-columns: 50px 8px 0 1fr;
- }
-
- .diff-grid-2-col {
- grid-template-columns: 100px 1fr !important;
-
- &.parallel {
- grid-template-columns: 50px 1fr !important;
- }
- }
-
- .diff-grid-comments {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
-
- .diff-grid-drafts {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
-
- &.inline-diff-view {
- .diff-grid-comments {
- display: grid;
- grid-template-columns: 1fr;
- }
-
- .diff-grid-drafts {
- display: grid;
- grid-template-columns: 1fr;
- }
-
- .diff-grid-row {
- grid-template-columns: 1fr;
- }
-
- .diff-grid-left,
- .diff-grid-right {
- // Zero width column is a placeholder for the EE inline code quality diff
- // see ee/../diffs.scss for more details
- grid-template-columns: 50px 50px 8px 0 1fr;
- }
- }
-}
-
-// Merge request diff grid layout overrides
-.diff-table.code .diff-tr.line_holder .diff-td.line_content.parallel {
- width: unset;
-}
-
.diff-stats {
align-items: center;
padding: 0 1rem;
@@ -730,68 +658,6 @@ table.code {
}
}
-.diff-comment-avatar-holders {
- position: absolute;
- margin-left: -$gl-padding;
- z-index: 100;
- @include code-icon-size();
-
- &:hover {
- .diff-comment-avatar,
- .diff-comments-more-count {
- @for $i from 1 through 4 {
- $x-pos: 14px;
-
- &:nth-child(#{$i}) {
- @if $i == 4 {
- $x-pos: 14.5px;
- }
-
- transform: translateX((($i * $x-pos) - $x-pos));
-
- &:hover {
- transform: translateX((($i * $x-pos) - $x-pos));
- }
- }
- }
- }
-
- .diff-comments-more-count {
- padding-left: 2px;
- padding-right: 2px;
- width: auto;
- }
- }
-}
-
-.diff-comment-avatar,
-.diff-comments-more-count {
- position: absolute;
- left: 0;
- margin-right: 0;
- border-color: $white;
- cursor: pointer;
- transition: all 0.1s ease-out;
- @include code-icon-size();
-
- @for $i from 1 through 4 {
- &:nth-child(#{$i}) {
- z-index: (4 - $i);
- }
- }
-
- .avatar {
- @include code-icon-size();
- }
-}
-
-.diff-comments-more-count {
- padding-left: 0;
- padding-right: 0;
- overflow: hidden;
- @include code-icon-size();
-}
-
.diff-comments-more-count,
.diff-notes-collapse,
.diff-codequality-collapse {
@@ -867,70 +733,6 @@ table.code {
}
}
-
-.diff-file-changes {
- max-width: 560px;
- width: 100%;
- z-index: 150;
- min-height: $dropdown-min-height;
- max-height: $dropdown-max-height;
- overflow-y: auto;
- margin-bottom: 0;
-
- @include media-breakpoint-up(sm) {
- left: $gl-padding;
- }
-
- .dropdown-input .dropdown-input-search {
- pointer-events: all;
- }
-
- .diff-changed-file {
- display: flex;
- padding-top: 8px;
- padding-bottom: 8px;
- min-width: 0;
- }
-
- .diff-file-changed-icon {
- margin-top: 2px;
- }
-
- .diff-changed-file-content {
- display: flex;
- flex-direction: column;
- min-width: 0;
- }
-
- .diff-changed-file-name,
- .diff-changed-blank-file-name {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .diff-changed-blank-file-name {
- color: $gl-text-color-tertiary;
- font-style: italic;
- }
-
- .diff-changed-file-path {
- color: $gl-text-color-tertiary;
- }
-
- .diff-changed-stats {
- margin-left: auto;
- white-space: nowrap;
- }
-}
-
-.diff-file-changes-path {
- flex: 1;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
.note-container {
background-color: $gray-light;
border-top: 1px solid $white-normal;
@@ -1007,27 +809,6 @@ table.code {
}
}
-// Notes tweaks for the Changes tab ONLY
-.diff-tr {
- .timeline-discussion-body {
- clear: left;
-
- .note-body {
- margin-top: 0 !important;
- }
- }
-
- .timeline-entry img.avatar {
- margin-top: -2px;
- margin-right: $gl-padding-8;
- }
-
- // tiny adjustment to vertical align with the note header text
- .discussion-collapsible .timeline-icon {
- padding-top: 2px;
- }
-}
-
.files:not([data-can-create-note]) .frame {
cursor: auto;
}
@@ -1097,6 +878,7 @@ table.code {
.discussion-notes {
min-height: 35px;
+ background-color: transparent;
&:first-child {
// First child does not have the jagged borders
@@ -1121,6 +903,17 @@ table.code {
display: none;
}
}
+
+ ul.notes {
+ li.toggle-replies-widget,
+ .discussion-reply-holder {
+ margin-left: 2.5rem;
+
+ .reply-author-avatar {
+ height: 1.5rem;
+ }
+ }
+ }
}
.discussion-body .image .frame {
@@ -1183,9 +976,15 @@ table.code {
bottom: 100vh;
}
-.diff-line-expand-button {
- &:hover,
- &:focus {
- @include gl-bg-gray-200;
+.diff-grid-row.expansion.match {
+ border-top: 1px solid var(--diff-expansion-background-color);
+ border-bottom: 1px solid var(--diff-expansion-background-color);
+
+ &:first-child {
+ border-top: 0;
+ }
+
+ &:last-child {
+ border-bottom: 0;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d91524d99e6..d561a7d9450 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -469,6 +469,7 @@
.sidebar-participant {
.merge-icon {
top: calc(50% + 5px);
+ left: 3rem;
}
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index b51daf0e4dc..b63365e8159 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -46,12 +46,14 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
.flash-notice,
.flash-success,
.flash-warning {
- padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
- margin-top: 10px;
-
- .container-fluid,
- .container-fluid.container-limited {
- background: transparent;
+ &:not(.gl-alert) {
+ padding: $gl-padding $gl-padding-32 $gl-padding ($gl-padding + $gl-padding-4);
+ margin-top: 10px;
+
+ .container-fluid,
+ .container-fluid.container-limited {
+ background: transparent;
+ }
}
}
@@ -79,6 +81,19 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
.gl-alert {
@include gl-my-4;
}
+
+ &.flash-container-no-margin {
+ .gl-alert {
+ @include gl-my-0;
+ }
+
+ .flash-alert,
+ .flash-notice,
+ .flash-success,
+ .flash-warning {
+ @include gl-mt-0;
+ }
+ }
}
@include media-breakpoint-down(sm) {
diff --git a/app/assets/stylesheets/framework/flex_grid.scss b/app/assets/stylesheets/framework/flex_grid.scss
deleted file mode 100644
index 10537fd5549..00000000000
--- a/app/assets/stylesheets/framework/flex_grid.scss
+++ /dev/null
@@ -1,52 +0,0 @@
-.flex-grid {
- .grid-row {
- border-bottom: 1px solid $border-color;
- padding: 0;
-
- &:last-child {
- border-bottom: 0;
- }
-
- @include media-breakpoint-down(md) {
- border-bottom: 0;
- border-right: 1px solid $border-color;
-
- &:last-child {
- border-right: 0;
- }
- }
-
- @include media-breakpoint-down(xs) {
- border-right: 0;
- border-bottom: 1px solid $border-color;
-
- &:last-child {
- border-bottom: 0;
- }
- }
- }
-
- .grid-cell {
- padding: 10px $gl-padding;
- border-right: 1px solid $border-color;
-
- &:last-child {
- border-right: 0;
- }
-
- @include media-breakpoint-up(md) {
- flex: 1;
- }
-
- @include media-breakpoint-down(md) {
- border-right: 0;
- flex: none;
- }
- }
-}
-
-.card {
- .card-body.flex-grid {
- padding: 0;
- }
-}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 40e11b50eba..66d163f608a 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -8,13 +8,15 @@
font-size: 95%;
}
-.gfm-project_member {
+.gfm-project_member,
+.md a.gfm-project_member {
padding: 0 2px;
background-color: $blue-100;
border-radius: $border-radius-default;
+ color: $blue-700;
&.current-user {
- background-color: $orange-50;
+ background-color: $orange-100;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index d2bb1e3d555..e9a507ebb6b 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -109,12 +109,6 @@ ul.content-list {
color: $gl-text-color;
word-break: break-word;
- &.no-description {
- .title {
- line-height: $list-text-height;
- }
- }
-
.title {
font-weight: $gl-font-weight-bold;
}
@@ -221,6 +215,7 @@ ul.content-list {
}
}
+ul.content-list.content-list-items-padding > li,
ul.content-list.issuable-list > li,
ul.content-list.todos-list > li,
.card > .content-list > li {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index b623f18c4ae..c40cadafb9c 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -44,12 +44,6 @@
}
}
-.div-dropzone-alert {
- margin-top: 5px;
- margin-bottom: 0;
- transition: opacity 200ms ease-in-out;
-}
-
.md-header {
.nav-links {
a {
@@ -155,8 +149,16 @@
.md-suggestion-diff {
display: table !important;
border: 1px solid $border-color !important;
- width: 100% !important;
- font-family: $monospace-font !important;
+
+ td {
+ border: 0 !important;
+ }
+
+ tr.old {
+ td {
+ border-radius: 0 !important;
+ }
+ }
}
.suggestions.md > .markdown-code-block {
@@ -164,23 +166,12 @@
}
.md-suggestion-header {
- height: $suggestion-header-height;
display: flex;
align-items: center;
justify-content: space-between;
background-color: $gray-light;
border: 1px solid $border-color;
- padding: $gl-padding;
border-radius: $border-radius-default $border-radius-default 0 0;
-
- svg {
- vertical-align: middle;
- margin-bottom: 3px;
- }
-
- .dropdown-chevron {
- margin-bottom: 0;
- }
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/memory_graph.scss b/app/assets/stylesheets/framework/memory_graph.scss
deleted file mode 100644
index 510969e149a..00000000000
--- a/app/assets/stylesheets/framework/memory_graph.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.memory-graph-container {
- background: $white;
- border: 1px solid $gray-100;
-}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index f39d53c5b1c..8b2a494527b 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -4,22 +4,6 @@
}
table {
- /*
- * TODO
- * This is a temporary workaround until we fix the neutral
- * color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
- *
- * The overwrites here affected the following areas:
- * - The subscription seats table. When removing this code, the .seats-table
- * <th> and margin overrides should be removed there.
- *
- * Remove this code as soon as this happens
- *
- */
- &.gl-table {
- @include gl-text-gray-500;
- }
-
&.table {
.thead-white {
th {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 43effbdd7d7..32e9bba8712 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -28,15 +28,9 @@
.timeline-entry {
color: $gl-text-color;
- // [dark-theme]: only give background color to actual notes
- // in the timeline, the note form textarea has a background
- // of it's own
- &:not(.note-form) {
- background-color: $white;
- }
-
- &:not(.note-form).internal-note {
- background-color: $orange-50;
+ &:not(.note-form).internal-note .timeline-content,
+ &:not(.note-form).draft-note .timeline-content {
+ background-color: $orange-50 !important;
}
.timeline-entry-inner {
@@ -45,23 +39,15 @@
&:target,
&.target {
- background: $line-target-blue;
+ .timeline-content {
+ background: $line-target-blue !important;
+ }
&.system-note .note-body .note-text.system-note-commit-list::after {
background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%);
}
}
- img.avatar {
- margin-right: $gl-padding-12;
-
- @include media-breakpoint-down(sm) {
- width: $gl-spacing-scale-6;
- height: $gl-spacing-scale-6;
- margin-right: $gl-padding-8;
- }
- }
-
.controls {
padding-top: 10px;
float: right;
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
deleted file mode 100644
index fd888fdec65..00000000000
--- a/app/assets/stylesheets/framework/toggle.scss
+++ /dev/null
@@ -1,131 +0,0 @@
-/**
-* Toggle button
-*
-* @usage
-* ### Active and Inactive text should be provided as data attributes:
-* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
-* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
-* </button>
-
-* ### Checked should have `is-checked` class
-* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
-* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
-* </button>
-
-* ### Disabled should have `is-disabled` class
-* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
-* <span class="gl-spinner loading-icon hidden" aria-label="Loading"></span>
-* </button>
-
-* ### Loading should have `is-loading` and an icon with `loading-icon` class
-* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
-* <span class="gl-spinner loading-icon" aria-label="Loading"></span>
-* </button>
-*/
-.project-feature-toggle {
- position: relative;
- border: 0;
- outline: 0;
- display: block;
- width: 50px;
- height: 24px;
- cursor: pointer;
- user-select: none;
- background: $gray-400;
- border-radius: 12px;
- padding: 3px;
- transition: all 0.4s ease;
-
- &::selection,
- &::before::selection,
- &::after::selection {
- background: none;
- }
-
- &:focus {
- outline: none;
- }
-
- .toggle-icon {
- position: relative;
- display: block;
- left: 0;
- border-radius: 9px;
- background: $white;
- transition: all 0.2s ease;
- width: $default-icon-size;
- height: $default-icon-size;
- }
-
- .loading-icon {
- display: none;
- font-size: 12px;
- color: $white;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
-
- &.is-loading {
- .loading-icon {
- display: block;
-
- &::before {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- }
- }
- }
-
- &.is-checked {
- background: $blue-400;
-
- .toggle-icon {
- left: calc(100% - 18px);
- }
- }
-
- &.is-checked .toggle-icon .toggle-status-checked,
- .toggle-icon .toggle-status-unchecked {
- display: inline;
- }
-
- &.is-checked .toggle-icon .toggle-status-unchecked,
- &.is-loading .toggle-icon,
- .toggle-icon .toggle-status-checked {
- display: none;
- }
-
- &.is-disabled {
- opacity: 0.4;
- cursor: not-allowed;
- }
-
- @include media-breakpoint-down(xs) {
- width: 50px;
-
- &::before,
- &.is-checked::before {
- display: none;
- }
- }
-
- @keyframes animate-enabled {
- 0%,
-
- 35% { opacity: 0; }
-
- 100% { opacity: 1; }
- }
-
- @keyframes animate-disabled {
- 0%,
-
- 35% { opacity: 0; }
-
- 100% { opacity: 1; }
- }
-}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index e79fb843967..2c2d8a2b592 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -33,14 +33,6 @@
}
}
- a {
- color: $blue-600;
-
- > code {
- color: $blue-600;
- }
- }
-
.media-container {
display: inline-flex;
flex-direction: column;
@@ -717,10 +709,6 @@ textarea.js-gfm-input {
font-size: $gl-font-size-monospace;
}
-.strikethrough {
- text-decoration: line-through;
-}
-
h1,
h2,
h3,
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bd32a817d5d..9cfc5a0201e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -382,6 +382,8 @@ $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: $white;
$gl-text-color-secondary-inverted: rgba($white, 0.85);
$gl-text-color-disabled: $gray-400;
+$link-color: $blue-500 !default;
+$link-hover-color: $blue-500 !default;
$gl-grayish-blue: #7f8fa4;
$gl-header-color: #4c4e54;
$gl-font-size-12: 12px;
@@ -440,7 +442,6 @@ $browser-scrollbar-size: 10px;
$header-height: var(--header-height, 48px);
$header-zindex: 1000;
$zindex-dropdown-menu: 300;
-$suggestion-header-height: 46px;
$ide-statusbar-height: 25px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
@@ -650,14 +651,6 @@ $calendar-border-color: rgba(#000, 0.1);
$calendar-user-contrib-text: #959494;
/*
- * Value Stream Analytics
- */
-$cycle-analytics-box-padding: 30px;
-$cycle-analytics-box-text-color: #8c8c8c;
-$cycle-analytics-big-font: 19px;
-$cycle-analytics-dismiss-icon-color: #b2b2b2;
-
-/*
* CI
*/
$ci-skipped-color: #888;
@@ -717,11 +710,11 @@ $job-arrow-margin: 55px;
*/
// See https://gitlab.com/gitlab-org/gitlab/-/issues/332150 to align with Pajamas Design System
$calendar-activity-colors: (
- #ededed,
- #acd5f2,
- #7fa8c9,
- #527ba0,
- #254e77,
+ #f5f5f5,
+ #d4dcfa,
+ #748eff,
+ #3547de,
+ #11118a,
) !default;
/*
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index cfd215b81b8..cb9c623c8fc 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -70,15 +70,6 @@
}
}
-.light-well {
- background-color: $gray-light;
- padding: 15px;
-}
-
-.dark-well {
- background-color: $gray-normal;
-}
-
.card.card-body-centered {
h1 {
font-weight: $gl-font-weight-normal;
diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss
index 5e6e10e44be..7fb2bf9a875 100644
--- a/app/assets/stylesheets/highlight/themes/dark.scss
+++ b/app/assets/stylesheets/highlight/themes/dark.scss
@@ -175,6 +175,8 @@ $dark-il: #de935f;
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-600};
+
@include dark-diff-expansion-line;
}
diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss
index 19c3d6926e7..66cada9181c 100644
--- a/app/assets/stylesheets/highlight/themes/monokai.scss
+++ b/app/assets/stylesheets/highlight/themes/monokai.scss
@@ -168,6 +168,8 @@ $monokai-gh: #75715e;
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-600};
+
@include dark-diff-expansion-line;
}
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index 4c716d20ddf..fa1f7211b3e 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -76,6 +76,10 @@
@include match-line;
}
+ &.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-100};
+ }
+
.line-coverage {
@include line-coverage-border-color($green-500, $orange-500);
}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
index 70086be1606..a1bba8720a2 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss
@@ -171,6 +171,8 @@ $solarized-dark-il: #2aa198;
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{lighten($solarized-dark-pre-bg, 10%)};
+
@include dark-diff-expansion-line;
}
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index 8d223d1fdb1..33945f7cda9 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -156,6 +156,10 @@ $solarized-light-il: #2aa198;
@include match-line;
}
+ &.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-100};
+ }
+
&.diff-grid-row.expansion .diff-td {
background-color: $solarized-light-matchline-bg;
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 9761e3961dd..816aa88cfde 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -154,6 +154,8 @@ pre.code,
}
&.diff-grid-row {
+ --diff-expansion-background-color: #{$gray-100};
+
@include diff-match-line;
}
diff --git a/app/assets/stylesheets/lazy_bundles/gridstack.scss b/app/assets/stylesheets/lazy_bundles/gridstack.scss
new file mode 100644
index 00000000000..235b225d747
--- /dev/null
+++ b/app/assets/stylesheets/lazy_bundles/gridstack.scss
@@ -0,0 +1 @@
+@import 'gridstack/dist/gridstack';
diff --git a/app/assets/stylesheets/page_bundles/admin/geo_nodes.scss b/app/assets/stylesheets/page_bundles/admin/geo_nodes.scss
new file mode 100644
index 00000000000..b0aaa48569a
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/admin/geo_nodes.scss
@@ -0,0 +1,45 @@
+@import '../mixins_and_variables_and_functions';
+
+.geo-node-header-grid-columns {
+ grid-template-columns: 1fr auto;
+ grid-gap: $gl-spacing-scale-5;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 3fr 1fr;
+ }
+}
+
+.geo-node-details-grid-columns {
+ grid-gap: $gl-spacing-scale-5;
+
+ @include media-breakpoint-up(lg) {
+ grid-template-columns: 1fr 3fr;
+ }
+}
+
+.geo-node-core-details-grid-columns {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: $gl-spacing-scale-5;
+}
+
+.geo-node-replication-details-grid-columns {
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 1rem;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 1fr 1fr 2fr 2fr;
+ }
+}
+
+.geo-node-filter-grid-columns {
+ grid-template-columns: 1fr;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 3fr 1fr;
+ }
+}
+
+.geo-node-replication-counts-grid {
+ grid-template-columns: 2fr 1fr 1fr;
+ grid-gap: 1rem;
+}
diff --git a/app/assets/stylesheets/page_bundles/admin/geo_replicable.scss b/app/assets/stylesheets/page_bundles/admin/geo_replicable.scss
new file mode 100644
index 00000000000..691d4abd195
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/admin/geo_replicable.scss
@@ -0,0 +1,18 @@
+@import '../mixins_and_variables_and_functions';
+
+.geo-replicable-item-grid {
+ grid-template-columns: 8ch 1fr auto;
+ grid-gap: 1rem;
+}
+
+.geo-replicable-filter-grid {
+ grid-template-columns: 1fr;
+
+ @include media-breakpoint-up(md) {
+ grid-template-columns: 2fr 1fr;
+ }
+
+ @include media-breakpoint-up(xl) {
+ grid-template-columns: 1fr 1fr;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/cluster_agents.scss b/app/assets/stylesheets/page_bundles/cluster_agents.scss
new file mode 100644
index 00000000000..d1fab55738f
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/cluster_agents.scss
@@ -0,0 +1,13 @@
+@import 'mixins_and_variables_and_functions';
+
+.agent-activity-list {
+ .system-note .timeline-entry-inner {
+ .timeline-icon {
+ @include gl-mt-1;
+ }
+ }
+
+ .timeline-entry::before {
+ @include gl-mt-4;
+ }
+}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss
index 27d81d8e53b..a877ae72e31 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/page_bundles/clusters.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.clusters-container {
@include media-breakpoint-down(xs) {
.nav-controls {
@@ -18,15 +20,3 @@
min-height: 372px;
}
}
-
-.agent-activity-list {
- .system-note .timeline-entry-inner {
- .timeline-icon {
- @include gl-mt-1;
- }
- }
-
- &.issuable-discussion .main-notes-list::before {
- @include gl-top-3;
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/graph_charts.scss b/app/assets/stylesheets/page_bundles/graph_charts.scss
new file mode 100644
index 00000000000..37a75f92a7e
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/graph_charts.scss
@@ -0,0 +1,27 @@
+@import 'page_bundles/mixins_and_variables_and_functions';
+
+.repo-charts {
+ .sub-header {
+ margin: 20px 0;
+ }
+
+ .sub-header-block.border-top {
+ margin-top: 20px;
+ padding: 0;
+ border-top: 1px solid var(--border-color, $border-color);
+ border-bottom: 0;
+ }
+
+ .commit-stats li {
+ font-size: 16px;
+ }
+
+ .tree-ref-header {
+ margin-bottom: 20px;
+
+ h4 {
+ margin: 0;
+ line-height: 36px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index a4a82fdcef3..3951f72112f 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -970,9 +970,6 @@ $ide-commit-header-height: 48px;
.ide-stage {
.card-header {
- display: flex;
- cursor: pointer;
-
.ci-status-icon {
display: flex;
align-items: center;
@@ -980,10 +977,6 @@ $ide-commit-header-height: 48px;
}
}
-.ide-stage-collapse-icon {
- margin: auto 0 auto auto;
-}
-
.ide-job-header {
min-height: 60px;
padding: 0 $gl-padding;
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
new file mode 100644
index 00000000000..de246fa14b9
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -0,0 +1,73 @@
+@import 'mixins_and_variables_and_functions';
+
+.issuable-discussion.incident-timeline-events {
+ .main-notes-list::before {
+ content: none;
+ }
+
+ .timeline-event-note {
+ p {
+ margin-bottom: 0;
+ font-size: 0.875rem;
+ }
+ }
+}
+
+/**
+ * We have a very specific design proposal where we cannot
+ * use `vertical-line` mixin as it is and have to use
+ * custom styles, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81284#note_904867444
+ */
+.timeline-entry-vertical-line {
+ &::before,
+ &::after {
+ content: '';
+ border-left: 2px solid $gray-50;
+ position: absolute;
+ left: 20px;
+ height: calc(100% + #{$gl-spacing-scale-5});
+ top: -#{$gl-spacing-scale-5};
+ }
+
+ &:first-child::before {
+ content: none;
+ }
+
+ &:first-child {
+ &::after {
+ top: $gl-spacing-scale-5;
+ height: calc(100% + #{$gl-spacing-scale-5});
+ }
+ }
+
+ &:last-child,
+ &.create-timeline-event {
+ &::before {
+ top: - #{$gl-spacing-scale-5} !important; // Override default positioning
+ @include gl-h-8;
+ }
+
+ &::after {
+ content: none;
+ }
+ }
+}
+
+.timeline-entry:not(:last-child) {
+ .timeline-event-border {
+ @include gl-pb-5;
+ @include gl-border-gray-50;
+ @include gl-border-1;
+ @include gl-border-b-solid;
+ }
+}
+
+.timeline-group:last-child {
+ .timeline-entry:last-child,
+ .create-timeline-event {
+ .timeline-event-bottom-border {
+ @include gl-border-b;
+ @include gl-pt-5;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index 26d694f7421..bbdcf1ea0c6 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -1,5 +1,9 @@
@import 'mixins_and_variables_and_functions';
+$design-pin-diameter: 28px;
+$design-pin-diameter-sm: 24px;
+$t-gray-a-16-design-pin: rgba($black, 0.16);
+
.description {
li {
position: relative;
@@ -23,6 +27,216 @@
}
}
+.design-card-header {
+ background: transparent;
+}
+
+.design-checkbox {
+ position: absolute;
+ top: $gl-padding;
+ left: 30px;
+}
+
+.layout-page.design-detail-layout {
+ max-height: 100vh;
+}
+
+.design-detail {
+ background-color: rgba($modal-backdrop-bg, $modal-backdrop-opacity);
+
+ .with-performance-bar & {
+ top: 35px;
+ }
+
+ .comment-indicator {
+ border-radius: 50%;
+ }
+
+ .comment-indicator,
+ .frame .design-note-pin {
+ &:active {
+ cursor: grabbing;
+ }
+ }
+}
+
+.design-list-item {
+ height: 280px;
+ text-decoration: none;
+
+ .icon-version-status {
+ position: absolute;
+ right: 10px;
+ top: 10px;
+ }
+
+ .card-body {
+ height: 230px;
+ }
+}
+
+// This is temporary class to be removed after feature flag removal: https://gitlab.com/gitlab-org/gitlab/-/issues/223197
+.design-list-item-new {
+ height: 210px;
+}
+
+.design-note-pin {
+ display: flex;
+ height: $design-pin-diameter;
+ width: $design-pin-diameter;
+ box-sizing: content-box;
+ background-color: var(--purple-500, $purple-500);
+ color: var(--white, $white);
+ font-weight: $gl-font-weight-bold;
+ border-radius: 50%;
+ z-index: 1;
+ padding: 0;
+ border: 0;
+
+ &.draft {
+ background-color: var(--orange-500, $orange-500);
+ }
+
+ &.resolved {
+ background-color: var(--gray-500, $gray-500);
+ }
+
+ &.on-image {
+ box-shadow: 0 2px 4px $t-gray-a-08, 0 0 1px $t-gray-a-24;
+ border: var(--white, $white) 2px solid;
+ will-change: transform, box-shadow, opacity;
+ // NOTE: verbose transition property required for Safari
+ transition: transform $general-hover-transition-duration linear, box-shadow $general-hover-transition-duration linear, opacity $general-hover-transition-duration linear;
+ transform-origin: 0 0;
+ transform: translate(-50%, -50%);
+
+ &:hover {
+ transform: scale(1.2) translate(-50%, -50%);
+ }
+
+ &:active {
+ box-shadow: 0 0 4px $t-gray-a-16-design-pin, 0 4px 12px $t-gray-a-16-design-pin;
+ }
+
+ &.inactive {
+ @include gl-opacity-5;
+
+ &:hover {
+ @include gl-opacity-10;
+ }
+ }
+ }
+
+ &.small {
+ position: absolute;
+ border: 1px solid var(--white, $white);
+ height: $design-pin-diameter-sm;
+ width: $design-pin-diameter-sm;
+ }
+
+ &.user-avatar {
+ top: 25px;
+ right: 8px;
+ }
+}
+
+.design-scaler-wrapper {
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.image-notes {
+ overflow-y: scroll;
+ padding: $gl-padding;
+ padding-top: 50px;
+ background-color: var(--white, $white);
+ flex-shrink: 0;
+ min-width: 400px;
+ flex-basis: 28%;
+
+ .link-inherit-color {
+ &:hover,
+ &:active,
+ &:focus {
+ color: inherit;
+ text-decoration: none;
+ }
+ }
+
+ .toggle-comments {
+ line-height: 20px;
+ border-top: 1px solid var(--border-color, $border-color);
+
+ &.expanded {
+ border-bottom: 1px solid var(--border-color, $border-color);
+ }
+
+ .toggle-comments-button:focus {
+ text-decoration: none;
+ color: var(--blue-600, $blue-600);
+ }
+ }
+
+ .design-note-pin {
+ margin-left: $gl-padding;
+ }
+
+ .design-discussion {
+ margin: $gl-padding 0;
+
+ &::before {
+ content: '';
+ border-left: 1px solid var(--gray-100, $gray-100);
+ position: absolute;
+ left: 28px;
+ top: -17px;
+ height: 17px;
+ }
+
+ .design-note {
+ padding: $gl-padding;
+ list-style: none;
+ transition: background $gl-transition-duration-medium $general-hover-transition-curve;
+ border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
+ border-top-right-radius: $border-radius-default;
+
+ a {
+ color: inherit;
+ }
+
+ .note-text a {
+ color: var(--blue-600, $blue-600);
+ }
+ }
+
+ .reply-wrapper {
+ padding: $gl-padding;
+ }
+ }
+
+ .reply-wrapper {
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+
+ .new-discussion-disclaimer {
+ line-height: 20px;
+ }
+}
+
+@media (max-width: map-get($grid-breakpoints, lg)) {
+ .design-detail {
+ overflow-y: scroll;
+ }
+
+ .image-notes {
+ overflow-y: auto;
+ min-width: 100%;
+ flex-grow: 1;
+ flex-basis: auto;
+ }
+}
+
.is-ghost {
opacity: 0.3;
pointer-events: none;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 463c8f74342..b2fbce7cb4b 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -9,6 +9,124 @@ $tabs-holder-z-index: 250;
min-width: 0;
}
+.diff-comment-avatar-holders {
+ position: absolute;
+ margin-left: -$gl-padding;
+ z-index: 100;
+ @include code-icon-size();
+
+ &:hover {
+ .diff-comment-avatar,
+ .diff-comments-more-count {
+ @for $i from 1 through 4 {
+ $x-pos: 14px;
+
+ &:nth-child(#{$i}) {
+ @if $i == 4 {
+ $x-pos: 14.5px;
+ }
+
+ transform: translateX((($i * $x-pos) - $x-pos));
+
+ &:hover {
+ transform: translateX((($i * $x-pos) - $x-pos));
+ }
+ }
+ }
+ }
+
+ .diff-comments-more-count {
+ padding-left: 2px;
+ padding-right: 2px;
+ width: auto;
+ }
+ }
+}
+
+.diff-comment-avatar,
+.diff-comments-more-count {
+ position: absolute;
+ left: 0;
+ margin-right: 0;
+ border-color: var(--white, $white);
+ cursor: pointer;
+ transition: all 0.1s ease-out;
+ @include code-icon-size();
+
+ @for $i from 1 through 4 {
+ &:nth-child(#{$i}) {
+ z-index: (4 - $i);
+ }
+ }
+
+ .avatar {
+ @include code-icon-size();
+ }
+}
+
+.diff-comments-more-count {
+ padding-left: 0;
+ padding-right: 0;
+ overflow: hidden;
+ @include code-icon-size();
+}
+
+.diff-file-changes {
+ max-width: 560px;
+ width: 100%;
+ z-index: 150;
+ min-height: $dropdown-min-height;
+ max-height: $dropdown-max-height;
+ overflow-y: auto;
+ margin-bottom: 0;
+
+ @include media-breakpoint-up(sm) {
+ left: $gl-padding;
+ }
+
+ .dropdown-input .dropdown-input-search {
+ pointer-events: all;
+ }
+
+ .diff-changed-file {
+ display: flex;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ min-width: 0;
+ }
+
+ .diff-file-changed-icon {
+ margin-top: 2px;
+ }
+
+ .diff-changed-file-content {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .diff-changed-file-name,
+ .diff-changed-blank-file-name {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .diff-changed-blank-file-name {
+ color: var(--gray-400, $gray-400);
+ font-style: italic;
+ }
+
+ .diff-changed-file-path {
+ color: var(--gray-400, $gray-400);
+ }
+
+ .diff-changed-stats {
+ margin-left: auto;
+ white-space: nowrap;
+ }
+}
+
.diff-files-holder {
flex: 1;
min-width: 0;
@@ -19,6 +137,111 @@ $tabs-holder-z-index: 250;
}
}
+.diff-grid {
+ .diff-td {
+ // By default min-width is auto with 1fr which causes some overflow problems
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/296222
+ min-width: 0;
+ }
+
+ .diff-grid-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ &.diff-grid-row-full {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ .diff-grid-left,
+ .diff-grid-right {
+ display: grid;
+ // Zero width column is a placeholder for the EE inline code quality diff
+ // see ee/.../diffs.scss for more details
+ grid-template-columns: 50px 8px 0 1fr;
+ }
+
+ .diff-grid-2-col {
+ grid-template-columns: 100px 1fr !important;
+
+ &.parallel {
+ grid-template-columns: 50px 1fr !important;
+ }
+ }
+
+ .diff-grid-comments {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ }
+
+ .diff-grid-drafts {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+
+ .content + .content {
+ @include gl-border-t;
+ }
+ }
+
+ &.inline-diff-view {
+ .diff-grid-comments {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .diff-grid-drafts {
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .diff-grid-row {
+ grid-template-columns: 1fr;
+ }
+
+ .diff-grid-left,
+ .diff-grid-right {
+ // Zero width column is a placeholder for the EE inline code quality diff
+ // see ee/../diffs.scss for more details
+ grid-template-columns: 50px 50px 8px 0 1fr;
+ }
+ }
+}
+
+.diff-line-expand-button {
+ &:hover,
+ &:focus {
+ background-color: var(--gray-200, $gray-200);
+ }
+}
+
+.diff-table.code .diff-tr.line_holder .diff-td.line_content.parallel {
+ width: unset;
+}
+
+.diff-tr {
+ .timeline-discussion-body {
+ clear: left;
+
+ .note-body {
+ padding: 0 0 $gl-padding-8;
+ }
+ }
+
+ .timeline-entry img.avatar {
+ margin-top: -2px;
+ margin-right: $gl-padding-8;
+ }
+
+ // tiny adjustment to vertical align with the note header text
+ .discussion-collapsible {
+ margin-left: 1rem;
+
+ .timeline-icon {
+ padding-top: 2px;
+ }
+ }
+}
+
.with-system-header {
--system-header-height: #{$system-header-height};
}
@@ -497,10 +720,6 @@ $tabs-holder-z-index: 250;
}
@include media-breakpoint-down(xs) {
- p {
- font-size: 13px;
- }
-
.btn-grouped {
float: none;
margin-right: 0;
@@ -661,10 +880,10 @@ $tabs-holder-z-index: 250;
&:not(:last-child)::before {
content: '';
- border-left: 1px solid var(--gray-100, $gray-100);
+ border-left: 2px solid var(--gray-10, $gray-10);
position: absolute;
- left: 28px;
bottom: -17px;
+ left: calc(1rem - 1px);
height: 16px;
}
}
@@ -677,7 +896,6 @@ $tabs-holder-z-index: 250;
display: flex;
align-items: center;
flex-wrap: wrap;
- padding: 16px;
z-index: 199;
white-space: nowrap;
@@ -833,6 +1051,12 @@ $tabs-holder-z-index: 250;
.detail-page-header-actions {
.gl-toggle {
@include gl-ml-auto;
+ @include gl-rounded-pill;
+ @include gl-w-9;
+
+ &.is-checked:hover {
+ background-color: $blue-500;
+ }
}
}
@@ -845,3 +1069,88 @@ $tabs-holder-z-index: 250;
@include gl-font-weight-normal;
}
}
+
+.dropdown-menu li button.gl-toggle:not(.is-checked) {
+ background: $gray-400;
+}
+
+.mr-widget-content-row:first-child {
+ border-top: 0;
+}
+
+.memory-graph-container {
+ background: var(--white, $white);
+ border: 1px solid var(--gray-100, $gray-100);
+}
+
+.review-bar-component {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ z-index: $zindex-dropdown-menu;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: $toggle-sidebar-height;
+ padding-left: $contextual-sidebar-width;
+ padding-right: $gutter_collapsed_width;
+ background: var(--white, $white);
+ border-top: 1px solid var(--border-color, $border-color);
+ transition: padding $gl-transition-duration-medium;
+
+ .page-with-icon-sidebar & {
+ padding-left: $contextual-sidebar-collapsed-width;
+ }
+
+ .right-sidebar-expanded & {
+ padding-right: $gutter_width;
+ }
+
+ @media (max-width: map-get($grid-breakpoints, sm)-1) {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ .dropdown {
+ margin-left: $grid-size;
+ }
+}
+
+.review-bar-content {
+ max-width: $limited-layout-width;
+ padding: 0 $gl-padding;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.review-preview-item-header {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 4px;
+
+ > .bold {
+ display: flex;
+ min-width: 0;
+ line-height: 16px;
+ }
+}
+
+.review-preview-item-footer {
+ display: flex;
+ align-items: center;
+ margin-top: 4px;
+}
+
+.review-preview-item-content {
+ width: 100%;
+
+ p {
+ display: block;
+ width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index c401f1a4902..63bcb83e747 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -1,4 +1,4 @@
-@import 'mixins_and_variables_and_functions';
+@import 'page_bundles/mixins_and_variables_and_functions';
$status-box-line-height: 26px;
@@ -40,39 +40,6 @@ $status-box-line-height: 26px;
}
}
}
-
- .card-header {
- line-height: $line-height-base;
- padding: 14px 16px;
- display: flex;
- justify-content: space-between;
-
- .title {
- flex: 1;
- flex-grow: 2;
- }
-
- .issuable-count-weight {
- white-space: nowrap;
-
- .counter,
- .weight {
- color: var(--gray-500, $gray-500);
- font-weight: $gl-font-weight-bold;
- }
- }
-
- &.text-white {
- .issuable-count-weight svg {
- fill: $white;
- }
-
- .issuable-count-weight .counter,
- .weight {
- color: var(--white, $white);
- }
- }
- }
}
.milestone-sidebar {
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/page_bundles/operations.scss
index 1dcaa47470b..497cb63033c 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/page_bundles/operations.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.dashboard-cards {
margin-right: -$gl-padding-8;
margin-left: -$gl-padding-8;
@@ -8,7 +10,7 @@
&-header {
&-warning {
- background-color: $orange-100;
+ background-color: var(--orange-100, $orange-100);
}
}
@@ -16,16 +18,16 @@
min-height: 120px;
&-warning {
- background-color: $orange-50;
+ background-color: var(--orange-50, $orange-50);
}
&-failed {
- background-color: $red-50;
+ background-color: var(--red-50, $red-50);
}
}
&-icon {
- color: $gray-300;
+ color: var(--gray-300, $gray-300);
}
&-footer {
@@ -33,7 +35,7 @@
height: $gl-padding-32;
&-arrow {
- color: $gray-200;
+ color: var(--gray-200, $gray-200);
}
&-downstream {
@@ -41,7 +43,7 @@
}
&-extra {
- background-color: $gray-200;
+ background-color: var(--gray-200, $gray-200);
font-size: 10px;
line-height: $gl-line-height;
width: $gl-padding;
@@ -50,7 +52,7 @@
&-header {
&-failed {
- background-color: $red-100;
+ background-color: var(--red-100, $red-100);
}
}
@@ -66,10 +68,10 @@
background-repeat: no-repeat;
background-size: cover;
background-image: linear-gradient(to right,
- $gray-50 0%,
- $gray-10 20%,
- $gray-50 40%,
- $gray-50 100%);
+ var(--gray-50, $gray-50) 0%,
+ var(--gray-10, $gray-10) 20%,
+ var(--gray-50, $gray-50) 40%,
+ var(--gray-50, $gray-50) 100%);
border-radius: $gl-padding;
height: $gl-padding;
margin-top: -$gl-padding-8;
diff --git a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
index 0c73bece035..af2dac7739e 100644
--- a/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
@@ -1,60 +1,82 @@
@import 'mixins_and_variables_and_functions';
-.pipeline-schedule-form {
- .gl-field-error {
- margin: 10px 0 0;
- }
+.ci-variable-list {
+ margin-left: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+ list-style: none;
+ clear: both;
}
-.interval-pattern-form-group {
- label {
- margin-right: 10px;
- font-weight: $gl-font-weight-normal;
+.ci-variable-row {
+ display: flex;
+ align-items: flex-start;
- &[for='custom'] {
- margin-right: 0;
- }
+ @include media-breakpoint-down(xs) {
+ align-items: flex-end;
}
- .cron-interval-input-wrapper {
- padding-left: 0;
- }
+ &:not(:last-child) {
+ margin-bottom: $gl-btn-padding;
- .cron-interval-input {
- margin: 10px 10px 0 0;
+ @include media-breakpoint-down(xs) {
+ margin-bottom: 3 * $gl-btn-padding;
+ }
}
-}
-.pipeline-schedule-table-row {
- .branch-name-cell {
- max-width: 300px;
- }
+ &:last-child {
+ .ci-variable-body-item:last-child {
+ margin-right: $ci-variable-remove-button-width;
- a {
- color: var(--gl-text-color, $gl-text-color);
- }
+ @include media-breakpoint-down(xs) {
+ margin-right: 0;
+ }
+ }
+
+ .ci-variable-row-remove-button {
+ display: none;
+ }
- svg {
- vertical-align: middle;
+ @include media-breakpoint-down(xs) {
+ .ci-variable-row-body {
+ margin-right: $ci-variable-remove-button-width;
+ }
+ }
}
}
-.pipeline-schedules-user-callout {
- .bordered-box.content-block {
- border: 1px solid var(--border-color, $border-color);
- background-color: transparent;
+.ci-variable-row-body {
+ display: flex;
+ align-items: flex-start;
+ width: 100%;
+ padding-bottom: $gl-padding;
+
+ @include media-breakpoint-down(xs) {
+ display: block;
}
}
-.cron-preset-radio-input {
- display: inline-block;
+.ci-variable-body-item {
+ flex: 1;
- @include media-breakpoint-down(md) {
- display: block;
- margin: 0 0 5px 5px;
+ &:not(:last-child) {
+ margin-right: $gl-btn-padding;
+
+ @include media-breakpoint-down(xs) {
+ margin-right: 0;
+ margin-bottom: $gl-btn-padding;
+ }
}
+}
- input {
- margin-right: 3px;
+.pipeline-schedule-form {
+ .gl-field-error {
+ margin: 10px 0 0;
+ }
+}
+
+.pipeline-schedule-table-row {
+ a {
+ color: var(--gl-text-color, $gl-text-color);
}
}
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index 59b8823c113..ac1e9fb024b 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -1,4 +1,33 @@
@import 'mixins_and_variables_and_functions';
+@import 'framework/buttons';
+
+.edit-user {
+ .emoji-menu-toggle-button {
+ @include emoji-menu-toggle-button;
+ }
+
+ @include media-breakpoint-down(sm) {
+ .input-md,
+ .input-lg {
+ max-width: 100%;
+ }
+ }
+}
+
+.modal-profile-crop {
+ .modal-dialog {
+ width: 380px;
+
+ @include media-breakpoint-down(xs) {
+ width: auto;
+ }
+ }
+
+ .profile-crop-image-container {
+ height: 300px;
+ margin: 0 auto;
+ }
+}
.calendar-block {
padding-left: 0;
@@ -210,3 +239,32 @@
.twitter-icon {
color: $twitter;
}
+
+.key-created-at {
+ line-height: 42px;
+}
+
+.key-list-item {
+ .key-list-item-info {
+ @include media-breakpoint-up(sm) {
+ float: left;
+ }
+ }
+}
+
+.ssh-keys-list {
+ .last-used-at,
+ .expires,
+ .key-created-at {
+ line-height: 32px;
+ }
+}
+
+.subkeys-list {
+ @include basic-list;
+
+ li {
+ padding: 3px 0;
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index eec5ebdb383..68bf2fa0f82 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -191,12 +191,4 @@
h5 {
color: var(--gl-text-color, $gl-text-color);
}
-
- .light-well {
- border-radius: 2px;
-
- color: var(--gray-600, $well-light-text-color);
- font-size: 13px;
- line-height: 1.6em;
- }
}
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/page_bundles/prometheus.scss
index 71cbd7d9613..702c0e4dd72 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/page_bundles/prometheus.scss
@@ -1,3 +1,11 @@
+@import 'mixins_and_variables_and_functions';
+
+.date-time-picker {
+ .date-time-picker-menu {
+ width: 400px;
+ }
+}
+
.prometheus-graphs {
.dropdown-buttons {
> div {
@@ -96,15 +104,6 @@
padding: $gl-padding-8;
}
-.alert-current-setting {
- max-width: 240px;
-
- .badge.badge-danger {
- color: $red-500;
- background-color: $red-100;
- }
-}
-
.prometheus-panel-builder {
.preview-date-time-picker {
// same as in .dropdown-menu-toggle
diff --git a/app/assets/stylesheets/components/release_block_milestone_info.scss b/app/assets/stylesheets/page_bundles/releases.scss
index b6a85ae965a..24ffbf9b90c 100644
--- a/app/assets/stylesheets/components/release_block_milestone_info.scss
+++ b/app/assets/stylesheets/page_bundles/releases.scss
@@ -1,3 +1,9 @@
+@import 'mixins_and_variables_and_functions';
+
+.release-block {
+ transition: background-color 1s linear;
+}
+
.release-block-milestone-info {
.milestone-progress-bar-container {
width: 300px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/page_bundles/tree.scss
index a9fbff8958d..58e55e11f7e 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/page_bundles/tree.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.project-last-commit {
min-height: 4.75rem;
}
@@ -100,11 +102,11 @@
margin-bottom: 0;
tr {
- border-bottom: 1px solid $white-normal;
- border-top: 1px solid $white-normal;
+ border-bottom: 1px solid var(--gray-50, $gray-50);
+ border-top: 1px solid var(--gray-50, $gray-50);
&:last-of-type {
- border-bottom-color: $white;
+ border-bottom-color: transparent;
}
td,
@@ -117,24 +119,24 @@
}
td {
- border-color: $border-color;
+ border-color: var(--border-color, $border-color);
}
&:hover:not(.tree-truncated-warning) {
td {
- background-color: $blue-50;
+ background-color: var(--blue-50, $blue-50);
background-clip: padding-box;
- border-top: 1px solid $blue-200;
- border-bottom: 1px solid $blue-200;
+ border-top: 1px solid var(--blue-200, $blue-200);
+ border-bottom: 1px solid var(--blue-200, $blue-200);
cursor: pointer;
}
}
&.selected {
td {
- background: $white-normal;
- border-top: 1px solid $border-white-normal;
- border-bottom: 1px solid $border-white-normal;
+ background: var(--gray-50, $gray-50);
+ border-top: 1px solid var(--border-color, $border-color);
+ border-bottom: 1px solid var(--border-color, $border-color);
}
}
}
@@ -156,7 +158,7 @@
i,
a {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
}
img {
@@ -175,22 +177,18 @@
}
.tree-truncated-warning {
- color: $orange-600;
- background-color: $orange-50;
+ color: var(--orange-600, $orange-600);
+ background-color: var(--orange-50, $orange-50);
}
.tree-time-ago {
min-width: 135px;
- color: $gl-text-color-secondary;
}
.tree-commit {
max-width: 320px;
- color: $gl-text-color-secondary;
.tree-commit-link {
- color: $gl-text-color-secondary;
-
&:hover {
text-decoration: underline;
}
@@ -207,40 +205,3 @@
.blob-content-holder {
margin-top: $gl-padding;
}
-
-.blob-upload-dropzone-previews {
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- border: 2px;
- border-style: dashed;
- border-color: $border-color;
- min-height: 200px;
-}
-
-.repo-charts {
- .sub-header {
- margin: 20px 0;
- }
-
- .sub-header-block.border-top {
- margin-top: 20px;
- padding: 0;
- border-top: 1px solid $white-dark;
- border-bottom: 0;
- }
-
- .commit-stats li {
- font-size: 16px;
- }
-
- .tree-ref-header {
- margin-bottom: 20px;
-
- h4 {
- margin: 0;
- line-height: 36px;
- }
- }
-}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index d0fc011dde7..820a1a0b53e 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -63,3 +63,22 @@
display: none;
}
}
+
+.work-item-dropdown {
+ .gl-dropdown-toggle {
+ background: none !important;
+
+ &:hover,
+ &:focus {
+ box-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-darkest, $gray-darkest) !important;
+ }
+
+ &.is-not-focused:not(:hover, :focus) {
+ box-shadow: none;
+
+ .gl-button-icon {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/deploy_keys.scss b/app/assets/stylesheets/pages/deploy_keys.scss
deleted file mode 100644
index 997e42a8fd5..00000000000
--- a/app/assets/stylesheets/pages/deploy_keys.scss
+++ /dev/null
@@ -1,4 +0,0 @@
-.deploy-keys-title {
- padding-bottom: 2px;
- line-height: 2;
-}
diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss
deleted file mode 100644
index f8f40076142..00000000000
--- a/app/assets/stylesheets/pages/environment_logs.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-.environment-logs-page {
- .content-wrapper {
- padding-bottom: 0;
- }
-}
-
-.environment-logs-viewer {
- height: calc(100vh - #{$environment-logs-difference-xs-up});
- min-height: 700px;
-
- @include media-breakpoint-up(md) {
- height: calc(100vh - #{$environment-logs-difference-md-up});
- }
-
- .with-performance-bar & {
- height: calc(100vh - #{$environment-logs-difference-xs-up} - #{$performance-bar-height});
-
- @include media-breakpoint-up(md) {
- height: calc(100vh - #{$environment-logs-difference-md-up} - #{$performance-bar-height});
- }
- }
-
- .top-bar {
- .date-time-picker-wrapper,
- .dropdown-toggle {
- @include media-breakpoint-up(md) {
- width: 140px;
- }
-
- @include media-breakpoint-up(lg) {
- width: 160px;
- }
- }
- }
-
- .log-lines,
- .gl-infinite-scroll-container {
- // makes scrollbar visible by creating contrast
- background: $black;
- height: 100%;
- }
-
- .build-log {
- @include build-log($black);
- }
-
- .gl-infinite-scroll-legend {
- margin: 0;
- }
-
- .build-loader-animation {
- @include build-loader-animation;
- }
-}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 33d00027404..ce8dd6684f2 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -84,6 +84,10 @@
iframe.twitter-share-button {
vertical-align: bottom;
}
+
+ .gl-label-scoped.gl-label-sm {
+ --label-inset-border: inset 0 0 0 1px currentColor;
+ }
}
code {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 85205f4d5ac..6070311dcb6 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -199,12 +199,15 @@
.sidebar-contained-width,
.issuable-sidebar-header {
width: 100%;
- border-bottom: 0;
}
.block {
@include media-breakpoint-up(lg) {
- padding: $gl-spacing-scale-5 0;
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ }
+
+ &.participants {
+ border-bottom: 0;
}
}
}
@@ -213,7 +216,8 @@
.sidebar-contained-width,
.issuable-sidebar-header {
@include clearfix;
- padding: $gl-padding 0;
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ border-bottom: 1px solid $border-gray-normal;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $gutter-inner-width;
@@ -235,6 +239,13 @@
}
}
}
+
+ &.time-tracking,
+ &.participants,
+ &.subscriptions,
+ &.with-sub-blocks {
+ padding-top: $gl-spacing-scale-5;
+ }
}
.block-first {
@@ -724,13 +735,7 @@
}
.issue-check {
- padding-right: $gl-padding;
- margin-bottom: 10px;
min-width: 15px;
-
- .selected-issuable {
- vertical-align: text-top;
- }
}
.issuable-milestone,
@@ -851,24 +856,6 @@
}
}
-.issuable-todo-btn {
- .gl-spinner {
- display: none;
- }
-
- &.is-loading {
- .gl-spinner {
- display: inline-block;
- }
-
- &.sidebar-collapsed-icon {
- .issuable-todo-inner {
- display: none;
- }
- }
- }
-}
-
/*
* Following overrides are done to prevent
* legacy dropdown styles from influencing
@@ -927,88 +914,3 @@
}
}
}
-
-.icon-overlap-and-shadow {
- filter:
- drop-shadow(0 1px 0.5px #fff)
- drop-shadow(1px 0 0.5px #fff)
- drop-shadow(0 -1px 0.5px #fff)
- drop-shadow(-1px 0 0.5px #fff);
- margin-right: -7px;
- z-index: 1;
-}
-
-.issuable-discussion.incident-timeline-events {
- .main-notes-list::before {
- content: none;
- }
-
- .timeline-event-note {
- p {
- margin-bottom: 0;
- }
- }
-}
-
-/**
- * We have a very specific design proposal where we cannot
- * use `vertical-line` mixin as it is and have to use
- * custom styles, see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81284#note_904867444
- */
-.timeline-entry-vertical-line {
- &::before,
- &::after {
- content: '';
- border-left: 2px solid $gray-50;
- position: absolute;
- left: 39px;
- height: calc(100% + #{$gl-spacing-scale-5});
- top: -#{$gl-spacing-scale-5};
- }
-
- &:first-child::before {
- content: none;
- }
-
- &:first-child {
- &::after {
- top: $gl-spacing-scale-5;
- height: calc(100% + #{$gl-spacing-scale-5});
- }
- }
-
- &:last-child,
- &.create-timeline-event {
- &::before {
- top: - #{$gl-spacing-scale-5} !important; // Override default positioning
- @include gl-h-8;
- }
-
- &::after {
- content: none;
- }
- }
-}
-
-.timeline-event-note-form {
- padding-left: 20px;
-}
-
-.timeline-entry:not(:last-child) {
- .timeline-event-border {
- @include gl-pb-5;
- @include gl-border-gray-50;
- @include gl-border-1;
- @include gl-border-b-solid;
- }
-}
-
-.timeline-group:last-child {
- .timeline-entry:last-child,
- .create-timeline-event {
- .timeline-event-bottom-border {
- @include gl-border-b;
- @include gl-pt-5;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 843daec8cda..c88834c088f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -123,6 +123,9 @@ ul.related-merge-requests > li gl-emoji {
}
.new-branch-col {
+ @include gl-pb-3;
+ @include gl-my-2;
+
.discussion-filter-container {
&:not(:last-child) {
margin-right: $gl-spacing-scale-3;
@@ -221,7 +224,7 @@ ul.related-merge-requests > li gl-emoji {
display: flex;
.new-branch-col {
- padding-top: 0;
+ @include gl-pb-0;
align-self: center;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index fc1b78bf730..438b7b1afa6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,15 +1,15 @@
-$system-note-icon-size: 32px;
-$system-note-svg-size: 16px;
+$system-note-icon-size: 2rem;
+$system-note-svg-size: 1rem;
@mixin vertical-line($left) {
&::before {
content: '';
- border-left: 2px solid $gray-10;
+ border-left: 2px solid var(--gray-10, $gray-10);
position: absolute;
top: 0;
bottom: 0;
- left: $left;
- height: calc(100% - 20px);
+ left: calc(#{$left} - 1px);
+ height: calc(100% + 1.5rem);
}
}
@@ -19,17 +19,10 @@ $system-note-svg-size: 16px;
border-radius: $border-radius-default;
}
-.note-wrapper {
- padding: $gl-padding $gl-padding-8 $gl-padding $gl-padding;
-
- &.outlined {
- @include outline-comment();
- }
-}
-
-.issuable-discussion {
- .main-notes-list {
- @include vertical-line(35px);
+.issuable-discussion:not(.incident-timeline-events),
+.limited-width-notes {
+ .main-notes-list > li.timeline-entry:not(:last-of-type) {
+ @include vertical-line(1rem);
}
}
@@ -41,8 +34,6 @@ $system-note-svg-size: 16px;
position: relative;
&.timeline > .timeline-entry {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
margin: $gl-padding 0;
&.system-note,
@@ -50,6 +41,117 @@ $system-note-svg-size: 16px;
border: 0;
}
+ .timeline-avatar {
+ height: 2rem;
+ }
+
+ &.note-comment,
+ &.note-skeleton,
+ .draft-note {
+ .timeline-avatar {
+ margin-top: 5px;
+ }
+
+ .timeline-content:not(.flash-container) {
+ margin-left: 2.5rem;
+ border: 1px solid $border-color;
+ border-radius: $gl-border-radius-base;
+ background-color: $white;
+ padding: $gl-padding-4 $gl-padding-8;
+ }
+
+ .note-header-info {
+ min-height: 2rem;
+ display: flex;
+ align-items: center;
+ gap: 0 0.25rem;
+ flex-wrap: wrap;
+ }
+ }
+
+ &.note-discussion {
+ .timeline-content .discussion-wrapper {
+ background-color: transparent;
+ }
+
+ .timeline-content {
+ ul li {
+ &:first-of-type {
+ .timeline-avatar {
+ margin-top: 5px;
+ }
+
+ .timeline-content {
+ margin-left: 2.5rem;
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ border-top: 1px solid $border-color;
+ border-top-left-radius: $gl-border-radius-base;
+ border-top-right-radius: $gl-border-radius-base;
+ background-color: $white;
+ padding: $gl-padding-4 $gl-padding-8;
+ }
+ }
+
+ &:not(:first-of-type) .timeline-entry-inner {
+ margin-left: 2.5rem;
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ background-color: $white;
+
+ .timeline-content {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+
+ .timeline-discussion-body {
+ margin-left: 2rem;
+ }
+ }
+ }
+
+ .diff-content {
+ ul li:first-of-type {
+ .timeline-avatar {
+ margin-top: 0;
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ border: 0;
+ padding: 0;
+ }
+
+ .timeline-entry-inner {
+ margin-left: 2.5rem;
+ border-left: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ background-color: $white;
+
+ .timeline-content {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+
+ .timeline-discussion-body {
+ margin-left: 2rem;
+ }
+ }
+ }
+ }
+ }
+
+ .discussion-reply-holder {
+ border: 1px solid $border-color;
+ }
+ }
+
&.note-form {
margin-left: 0;
@@ -88,10 +190,14 @@ $system-note-svg-size: 16px;
.card {
margin-bottom: 0;
}
- }
- .timeline-discussion-body {
- margin-top: -$gl-padding-8;
+ .note-header-info {
+ min-height: 2rem;
+ display: flex;
+ align-items: center;
+ gap: 0 0.25rem;
+ flex-wrap: wrap;
+ }
}
.discussion {
@@ -116,16 +222,11 @@ $system-note-svg-size: 16px;
&.being-posted {
pointer-events: none;
opacity: 0.5;
- padding: $gl-padding;
.dummy-avatar {
background-color: $gray-100;
border: 1px solid darken($gray-100, 25%);
}
-
- .note-headline-light {
- margin-left: 3px;
- }
}
.editing-spinner {
@@ -156,6 +257,7 @@ $system-note-svg-size: 16px;
.note-edit-form {
display: block;
margin-left: 0;
+ margin-top: 0.5rem;
&.current-note-edit-form + .note-awards {
display: none;
@@ -164,13 +266,17 @@ $system-note-svg-size: 16px;
}
.note-body {
- padding: $gl-padding-4 $gl-padding-4 $gl-padding-4 $gl-padding-8;
+ padding: 0 $gl-padding-8 $gl-padding-8;
overflow-x: auto;
overflow-y: hidden;
.note-text {
word-wrap: break-word;
}
+
+ .suggestions {
+ margin-top: 4px;
+ }
}
.note-awards {
@@ -186,9 +292,10 @@ $system-note-svg-size: 16px;
}
.system-note {
- padding: $gl-padding-4 20px;
+ padding: $gl-padding-8 0;
margin: $gl-padding 0;
background-color: transparent;
+ font-size: $gl-font-size;
.note-header-info {
padding-bottom: 0;
@@ -229,6 +336,15 @@ $system-note-svg-size: 16px;
.note-body {
overflow: hidden;
+ padding: 0;
+
+ ul {
+ margin: 0.5rem 0;
+ }
+
+ p {
+ margin-left: 1rem;
+ }
.description-version {
position: relative;
@@ -305,7 +421,7 @@ $system-note-svg-size: 16px;
height: $system-note-icon-size;
border: 1px solid $gray-10;
border-radius: $system-note-icon-size;
- margin: -6px 0 0;
+ margin: -8px 0 0;
svg {
width: $system-note-svg-size;
@@ -319,25 +435,38 @@ $system-note-svg-size: 16px;
.discussion-filter-note {
.timeline-icon {
- width: $system-note-icon-size + 6;
- height: $system-note-icon-size + 6;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
margin-top: -8px;
}
}
}
+.card .notes {
+ .system-note {
+ margin: 0;
+ padding: 0;
+ }
+
+ .timeline-icon {
+ margin: 8px 0 0 14px;
+ }
+}
+
+
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
cursor: default;
- border-top: 1px solid $border-color;
+ border-top: 0;
border-radius: 0;
+ margin-left: 2.5rem;
@media (min-width: map-get($grid-breakpoints, md)) {
--initial-top: calc(#{$header-height} + #{$mr-tabs-height});
&.is-sidebar-moved {
- --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 28px});
+ --initial-top: calc(#{$header-height} + #{$mr-tabs-height + 24px});
}
.with-performance-bar & {
@@ -357,6 +486,40 @@ $system-note-svg-size: 16px;
.line_content {
white-space: pre-wrap;
}
+
+ .diff-content {
+ margin-left: 2.5rem;
+
+ &.outdated-lines-wrapper {
+ margin-left: 0;
+ }
+
+ .line_holder td:first-of-type {
+ @include gl-border-l;
+ }
+
+ .line_holder td:last-of-type {
+ @include gl-border-r;
+ }
+
+ .discussion-notes {
+ margin-left: -2.5rem;
+
+ .notes {
+ background-color: transparent;
+ }
+
+ .notes-content {
+ border: 0;
+ }
+
+ .timeline-content {
+ border-top: 0 !important;
+ border-top-left-radius: 0 !important;
+ border-top-right-radius: 0 !important;
+ }
+ }
+ }
}
.tab-pane.notes {
@@ -394,8 +557,17 @@ $system-note-svg-size: 16px;
}
.system-note {
- background-color: $white;
- padding: $gl-padding;
+ background-color: transparent;
+ padding: 0;
+
+ .timeline-icon {
+ margin-top: -2px;
+ }
+
+ .timeline-entry-inner .timeline-icon {
+ margin-top: $grid-size;
+ margin-left: 14px;
+ }
}
}
@@ -487,6 +659,19 @@ $system-note-svg-size: 16px;
.code-commit .notes-content,
.diff-viewer > .image ~ .note-container {
background-color: $white;
+
+ li.note-comment {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+
+ .avatar {
+ margin-right: 0;
+ }
+
+ .note-body {
+ padding: $gl-padding-4 0 $gl-padding-8;
+ margin-left: 2.5rem;
+ }
+ }
}
.diff-viewer > .image ~ .note-container form.new-note {
@@ -540,9 +725,21 @@ $system-note-svg-size: 16px;
padding-bottom: 0;
}
+ .timeline-avatar {
+ margin-top: 5px;
+ }
+
.timeline-content {
overflow-x: auto;
overflow-y: hidden;
+ border-radius: $gl-border-radius-base;
+ padding: $gl-padding-8 !important;
+ @include gl-border;
+
+ &.expanded {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
}
&.note-wrapper {
@@ -568,19 +765,10 @@ $system-note-svg-size: 16px;
.note {
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
.note-header {
- .note-actions {
- flex-wrap: wrap;
- margin-bottom: $gl-padding-12;
-
- > :first-child {
- margin-left: 0;
- }
+ .note-actions > :first-child {
+ margin-left: 0;
}
}
-
- .note-header-author-name {
- display: block;
- }
}
}
@@ -593,11 +781,6 @@ $system-note-svg-size: 16px;
}
}
-.note-header-info,
-.note-actions {
- padding-bottom: $gl-padding-4;
-}
-
.system-note .note-header-info {
padding-bottom: 0;
}
@@ -618,10 +801,6 @@ $system-note-svg-size: 16px;
}
.note-headline-meta {
- .system-note-separator {
- color: $gray-500;
- }
-
.note-timestamp {
white-space: nowrap;
}
@@ -667,18 +846,20 @@ $system-note-svg-size: 16px;
}
.note-actions {
- align-self: flex-start;
justify-content: flex-end;
flex-shrink: 1;
display: inline-flex;
align-items: center;
- margin-left: 10px;
+ margin-left: $gl-padding-8;
color: $gray-400;
- margin-top: -4px;
@include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
+ justify-content: flex-start;
float: none;
- margin-left: 0;
+
+ .note-actions__mobile-spacer {
+ flex-grow: 1;
+ }
}
}
@@ -719,7 +900,7 @@ $system-note-svg-size: 16px;
}
.discussion-toggle-button {
- padding: 0;
+ padding: 0 $gl-padding-8 0 0;
background-color: transparent;
border: 0;
line-height: 20px;
@@ -868,6 +1049,28 @@ $system-note-svg-size: 16px;
.note-discussion.timeline-entry {
padding-left: 0;
+ ul.notes li.note-wrapper {
+ .timeline-content {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+ }
+
+ ul.notes {
+ li.toggle-replies-widget {
+ margin-left: 0;
+ border-left: 0;
+ border-right: 0;
+ }
+
+ div.discussion-reply-holder {
+ margin-left: 0;
+ }
+ }
+
&:last-child {
border-bottom: 0;
}
@@ -894,6 +1097,16 @@ $system-note-svg-size: 16px;
}
}
+ .draft-note-component .draft-note.timeline-entry {
+ .timeline-content:not(.flash-container) {
+ padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
+ }
+
+ .timeline-avatar {
+ margin: $gl-padding-8 0 0 $gl-padding;
+ }
+ }
+
.diff-comment-form {
display: block;
}
@@ -909,8 +1122,7 @@ $system-note-svg-size: 16px;
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/53918#note_117038785
.unstyled-comments {
.discussion-header {
- padding: $gl-padding;
- border-bottom: 1px solid $border-color;
+ padding: $gl-padding 0;
}
.discussion-form-container {
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 951e31ef768..8e4dd39e498 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,22 +1,3 @@
-.avatar-image {
- margin-bottom: $grid-size;
-
- .avatar {
- float: none;
- }
-
- @include media-breakpoint-up(sm) {
- float: left;
- margin-bottom: 0;
- }
-}
-
-.avatar-file-name {
- position: relative;
- top: 2px;
- display: inline-block;
-}
-
.account-well {
padding: 10px;
background-color: $gray-light;
@@ -29,42 +10,6 @@
}
}
-.user-avatar-button {
- .file-name {
- display: inline-block;
- padding-left: 10px;
- }
-}
-
-.subkeys-list {
- @include basic-list;
-
- li {
- padding: 3px 0;
- border: 0;
- }
-}
-
-.key-list-item {
- .key-list-item-info {
- @include media-breakpoint-up(sm) {
- float: left;
- }
- }
-}
-
-.ssh-keys-list {
- .last-used-at,
- .expires,
- .key-created-at {
- line-height: 32px;
- }
-}
-
-.key-created-at {
- line-height: 42px;
-}
-
.provider-btn-group {
display: inline-block;
margin-right: 10px;
@@ -113,26 +58,6 @@
}
}
-.modal-profile-crop {
- .modal-dialog {
- width: 380px;
-
- @include media-breakpoint-down(xs) {
- width: auto;
- }
- }
-
- .profile-crop-image-container {
- height: 300px;
- margin: 0 auto;
- }
-
- .crop-controls {
- padding: 10px 0 0;
- text-align: center;
- }
-}
-
.created-personal-access-token-container {
.btn-clipboard {
border: 1px solid $border-color;
@@ -247,36 +172,6 @@ table.u2f-registrations {
}
}
-.edit-user {
- svg {
- fill: $gl-text-color-secondary;
- }
-
- .form-group > label {
- font-weight: $gl-font-weight-bold;
- }
-
- .form-group > .form-text {
- font-size: $gl-font-size;
- }
-
- .emoji-menu-toggle-button {
- @include emoji-menu-toggle-button;
- padding: 6px 10px;
-
- .no-emoji-placeholder {
- position: relative;
- }
- }
-
- @include media-breakpoint-down(sm) {
- .input-md,
- .input-lg {
- max-width: 100%;
- }
- }
-}
-
.help-block {
color: $gl-text-color-secondary;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0d45beab983..be8707dcd50 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -269,49 +269,27 @@
color: $gray-200;
}
-pre.light-well {
- border-color: $well-light-border;
-}
-
/*
* Projects list rendered on dashboard and user page
*/
+
+.project-row {
+ .description p {
+ margin-bottom: 0;
+ color: $gl-text-color-secondary;
+ }
+}
+
.projects-list {
@include basic-list;
display: flex;
flex-direction: column;
- // Disable Flexbox for admin page
- &.admin-projects,
- &.group-settings-projects {
- display: block;
-
- .project-row {
- display: block;
-
- .description > p {
- margin-bottom: 0;
- }
- }
- }
-
.project-row {
@include basic-list-stats;
display: flex;
align-items: center;
padding: $gl-padding-12 0;
-
- &.no-description {
- @include media-breakpoint-up(sm) {
- .avatar-container {
- align-self: center;
- }
-
- .metadata-info {
- margin-bottom: 0;
- }
- }
- }
}
h2 {
@@ -634,24 +612,6 @@ pre.light-well {
}
}
-.clearable-input {
- position: relative;
-
- .clear-icon {
- display: none;
- position: absolute;
- right: 9px;
- top: 9px;
- }
-
- &.has-value {
- .clear-icon {
- cursor: pointer;
- display: block;
- }
- }
-}
-
.project-path {
.form-control {
min-width: 100px;
@@ -810,10 +770,3 @@ pre.light-well {
}
}
}
-
-@include media-breakpoint-down(xs) {
- .fork-filtered-search {
- width: 100%;
- margin: $gl-spacing-scale-2 0;
- }
-}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index e8f71c8a21c..a8027d2a5f5 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -268,16 +268,6 @@ input[type='checkbox']:hover {
}
}
- .search-clear {
- position: absolute;
- right: 10px;
- top: 9px;
- padding: 0;
- line-height: 0;
- background: none;
- border: 0;
- }
-
.search-icon {
position: absolute;
left: 10px;
@@ -327,15 +317,6 @@ input[type='checkbox']:hover {
}
}
-.search-clear {
- color: $gray-darkest;
-
- &:hover,
- &:focus {
- color: $blue-600;
- }
-}
-
.search-page-form {
.dropdown-menu-toggle,
.btn-search {
diff --git a/app/assets/stylesheets/pages/service_desk.scss b/app/assets/stylesheets/pages/service_desk.scss
deleted file mode 100644
index 34ab5eb1b74..00000000000
--- a/app/assets/stylesheets/pages/service_desk.scss
+++ /dev/null
@@ -1,7 +0,0 @@
-.service-desk-issues {
- .non-empty-state {
- text-align: left;
- padding-bottom: $gl-padding-top;
- border-bottom: 1px solid $border-color;
- }
-}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 56acf6de828..c364b233803 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -188,31 +188,6 @@
}
}
-.nested-settings {
- padding-left: 20px;
-}
-
-.input-btn-group {
- display: flex;
-
- .input-large {
- flex: 1;
- }
-
- .btn {
- margin-left: 10px;
- }
-}
-
-.content-list > .settings-flex-row {
- display: flex;
- align-items: center;
-
- .float-right {
- margin-left: auto;
- }
-}
-
.prometheus-metrics-monitoring {
.card {
.card-toggle {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 0450b3d9a44..32c3ce1ba8c 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -57,7 +57,7 @@ strong {
font-weight: bolder;
}
a {
- color: #007bff;
+ color: #428fdc;
text-decoration: none;
background-color: transparent;
}
@@ -368,6 +368,23 @@ kbd kbd {
white-space: nowrap;
border: 0;
}
+.gl-avatar {
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+.gl-avatar-s24 {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ border-radius: 0.25rem;
+}
+.gl-avatar-circle {
+ border-radius: 50%;
+}
.gl-badge {
display: inline-flex;
align-items: center;
@@ -552,9 +569,6 @@ html [type="button"],
strong {
font-weight: bold;
}
-a {
- color: #63a6e9;
-}
svg {
vertical-align: baseline;
}
@@ -1783,10 +1797,15 @@ body.gl-dark {
background-color: #262626;
border-right: 1px solid #303030;
}
+.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
background: rgba(255, 255, 255, 0.04);
}
+.gl-avatar {
+ border-style: none;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
+}
body.gl-dark {
--gl-theme-accent: #868686;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 356fb58b4c8..61a2ce8dd62 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -38,7 +38,7 @@ strong {
font-weight: bolder;
}
a {
- color: #007bff;
+ color: #1f75cb;
text-decoration: none;
background-color: transparent;
}
@@ -349,6 +349,23 @@ kbd kbd {
white-space: nowrap;
border: 0;
}
+.gl-avatar {
+ border-width: 1px;
+ border-style: solid;
+ border-color: rgba(0, 0, 0, 0.08);
+ overflow: hidden;
+ flex-shrink: 0;
+}
+.gl-avatar-s24 {
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ border-radius: 0.25rem;
+}
+.gl-avatar-circle {
+ border-radius: 50%;
+}
.gl-badge {
display: inline-flex;
align-items: center;
@@ -533,9 +550,6 @@ html [type="button"],
strong {
font-weight: bold;
}
-a {
- color: #1068bf;
-}
svg {
vertical-align: baseline;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index edc579f48f6..33e10b9bd62 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -41,7 +41,7 @@ p {
margin-bottom: 1rem;
}
a {
- color: #007bff;
+ color: #1f75cb;
text-decoration: none;
background-color: transparent;
}
@@ -498,7 +498,7 @@ input.btn-block[type="button"] {
.custom-control-input:checked:disabled
~ .custom-control-label::before,
.gl-form-checkbox.custom-control
- .custom-control-input:indeterminate:disabled
+ .custom-control-input[type="checkbox"]:indeterminate:disabled
~ .custom-control-label::before {
background-color: #dbdbdb;
border-color: #dbdbdb;
@@ -507,7 +507,7 @@ input.btn-block[type="button"] {
.custom-control-input:checked:disabled
~ .custom-control-label::after,
.gl-form-checkbox.custom-control
- .custom-control-input:indeterminate:disabled
+ .custom-control-input[type="checkbox"]:indeterminate:disabled
~ .custom-control-label::after {
background-color: #5e5e5e;
}
@@ -595,9 +595,6 @@ h3 {
margin-top: 20px;
margin-bottom: 10px;
}
-a {
- color: #1068bf;
-}
hr {
overflow: hidden;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 4b74e449e06..8e8cabbe511 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -269,9 +269,9 @@ $well-expand-item: $gray-200;
$well-inner-border: $gray-200;
$calendar-activity-colors: (
- #303030,
- #333861,
- #4a5593,
- #6172c5,
- #788ff7
+ #404040,
+ #1e23a8,
+ #445cf2,
+ #97acff,
+ #e9ebff
);
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index e1ba2a69420..a0d19c3de2a 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -141,7 +141,8 @@ body.gl-dark {
}
}
-.timeline-entry.internal-note:not(.note-form) {
+.timeline-entry.internal-note:not(.note-form) .timeline-content,
+.timeline-entry.draft-note:not(.note-form) .timeline-content {
// soften on darkmode
- background-color: mix($gray-50, $orange-50, 75%);
+ background-color: mix($gray-50, $orange-50, 75%) !important;
}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index bdb8f758137..4be4fc82d04 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -75,10 +75,8 @@
// .gl-font-size-inherit will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1466
.gl-font-size-inherit,
.font-size-inherit { font-size: inherit; }
-.gl-w-8 { width: px-to-rem($grid-size); }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); }
-.gl-h-8 { height: px-to-rem($grid-size); }
.gl-h-32 { height: px-to-rem($grid-size * 4); }
.gl-h-64 { height: px-to-rem($grid-size * 8); }
@@ -119,13 +117,6 @@
flex-basis: 25%;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
-.gl-md-ml-3 {
- @media (min-width: $breakpoint-md) {
- margin-left: $gl-spacing-scale-3;
- }
-}
-
// Will be moved to @gitlab/ui (without the !important) in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1462
// We only need the bang (!) version until the non-bang version is added to
// @gitlab/ui utitlities.scss. Once there, it will get loaded in the correct
@@ -152,48 +143,6 @@
display: flex;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1085
-.gl-md-flex-direction-column {
- @media (min-width: $breakpoint-md) {
- flex-direction: column;
- }
-}
-
-// Same as above
-.gl-md-flex-direction-column\! {
- @media (min-width: $breakpoint-md) {
- flex-direction: column !important;
- }
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1165
-.gl-xs-mb-4 {
- @media (max-width: $breakpoint-sm) {
- margin-bottom: $gl-spacing-scale-4;
- }
-}
-
-// Same as above
-.gl-xs-mb-4\! {
- @media (max-width: $breakpoint-sm) {
- margin-bottom: $gl-spacing-scale-4 !important;
- }
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
-.gl-sm-pr-3 {
- @media (min-width: $breakpoint-sm) {
- padding-right: $gl-spacing-scale-3;
- }
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1168
-.gl-sm-w-half {
- @media (min-width: $breakpoint-sm) {
- width: 50%;
- }
-}
-
.gl-sm-mr-3 {
@include media-breakpoint-up(sm) {
margin-right: $gl-spacing-scale-3;
@@ -206,21 +155,10 @@
}
}
-.gl-mb-n3 {
- margin-bottom: -$gl-spacing-scale-3;
-}
-
.gl-mr-n2 {
margin-right: -$gl-spacing-scale-2;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1408
-$gl-line-height-42: px-to-rem(42px);
-
-.gl-line-height-42 {
- line-height: $gl-line-height-42;
-}
-
.gl-w-grid-size-30 {
width: $grid-size * 30;
}
@@ -229,26 +167,6 @@ $gl-line-height-42: px-to-rem(42px);
width: $grid-size * 40;
}
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
-.gl-max-w-none\! {
- max-width: none !important;
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2209
-.gl-max-h-none\! {
- max-height: none !important;
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1655
-.gl-max-w-62 {
- max-width: $grid-size * 62;
-}
-
-// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1655
-.gl-max-w-26 {
- max-width: $grid-size * 26;
-}
-
.gl-max-w-50p {
max-width: 50%;
}
@@ -271,36 +189,15 @@ $gl-line-height-42: px-to-rem(42px);
}
}
-// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
-.gl-text-transparent {
- color: transparent;
-}
-
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
.gl-focus-ring-border-1-gray-900\! {
@include gl-focus($gl-border-size-1, $gray-900, true);
}
-// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2600
-.gl-pr-10 {
- padding-right: $gl-spacing-scale-10;
-}
-
/*
All of the following (up until the "End gitlab-ui#1709" comment) will be moved
to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
*/
-.gl-sm-grid-template-columns-2 {
- @include media-breakpoint-up(sm) {
- grid-template-columns: 1fr 1fr;
- }
-}
-
-.gl-md-grid-template-columns-2 {
- @include media-breakpoint-up(md) {
- grid-template-columns: 1fr 1fr;
- }
-}
-
.gl-md-grid-template-columns-3 {
@include media-breakpoint-up(md) {
grid-template-columns: repeat(3, 1fr);
@@ -313,10 +210,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
-.gl-gap-6 {
- gap: $gl-spacing-scale-6;
-}
-
.gl-max-w-48 {
max-width: $gl-spacing-scale-48;
}
@@ -346,18 +239,6 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
/* End gitlab-ui#1709 */
/*
- * The below two styles will be moved to @gitlab/ui by
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1750
- */
-.gl-max-w-34 {
- max-width: 34 * $grid-size;
-}
-
-.gl-max-w-80 {
- max-width: 80 * $grid-size;
-}
-
-/*
* The below style will be moved to @gitlab/ui by
* https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1751
*/
@@ -370,13 +251,3 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
.gl-flex-flow-row-wrap {
flex-flow: row wrap;
}
-
-/*
- * The below style will be moved to @gitlab/ui by
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1963
- */
-.gl-gap-y-3 {
- > * + * {
- margin-top: $gl-spacing-scale-3;
- }
-}