summaryrefslogtreecommitdiff
path: root/app/assets
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-20 13:18:24 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-20 13:18:24 +0000
commit0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch)
tree4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/assets
parent744144d28e3e7fddc117924fef88de5d9674fe4c (diff)
downloadgitlab-ce-0653e08efd039a5905f3fa4f6e9cef9f5d2f799c.tar.gz
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/learn_gitlab/code_owners_enabled.svg5
-rw-r--r--app/assets/images/learn_gitlab/git_write.svg16
-rw-r--r--app/assets/images/learn_gitlab/issue_created.svg65
-rw-r--r--app/assets/images/learn_gitlab/merge_request_created.svg107
-rw-r--r--app/assets/images/learn_gitlab/pipeline_created.svg38
-rw-r--r--app/assets/images/learn_gitlab/required_mr_approvals_enabled.svg70
-rw-r--r--app/assets/images/learn_gitlab/security_scan_enabled.svg36
-rw-r--r--app/assets/images/learn_gitlab/trial_started.svg9
-rw-r--r--app/assets/images/learn_gitlab/user_added.svg4
-rw-r--r--app/assets/javascripts/alert_management/list.js1
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js5
-rw-r--r--app/assets/javascripts/analytics/usage_trends/index.js2
-rw-r--r--app/assets/javascripts/api.js8
-rw-r--r--app/assets/javascripts/api/projects_api.js8
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue2
-rw-r--r--app/assets/javascripts/autosave.js2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue11
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue3
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue10
-rw-r--r--app/assets/javascripts/batch_comments/constants.js2
-rw-r--r--app/assets/javascripts/batch_comments/index.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/nodes/playable.js40
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue2
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js1
-rw-r--r--app/assets/javascripts/blob/notebook/index.js3
-rw-r--r--app/assets/javascripts/blob/notebook/notebook_viewer.vue6
-rw-r--r--app/assets/javascripts/blob/pipeline_tour_success_modal.vue4
-rw-r--r--app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue2
-rw-r--r--app/assets/javascripts/boards/boards_util.js12
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue32
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue29
-rw-r--r--app/assets/javascripts/boards/components/board_card_deprecated.vue61
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_card_layout_deprecated.vue101
-rw-r--r--app/assets/javascripts/boards/components/board_column_deprecated.vue112
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue31
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue19
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue459
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header_deprecated.vue361
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue_deprecated.vue138
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue53
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js115
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue360
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue8
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue47
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue247
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue48
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js119
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue146
-rw-r--r--app/assets/javascripts/boards/config_toggle.js3
-rw-r--r--app/assets/javascripts/boards/constants.js5
-rw-r--r--app/assets/javascripts/boards/ee_functions.js4
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js15
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/boards/graphql/project_milestones.query.graphql2
-rw-r--r--app/assets/javascripts/boards/index.js344
-rw-r--r--app/assets/javascripts/boards/models/assignee.js13
-rw-r--r--app/assets/javascripts/boards/models/issue.js99
-rw-r--r--app/assets/javascripts/boards/models/iteration.js9
-rw-r--r--app/assets/javascripts/boards/models/label.js11
-rw-r--r--app/assets/javascripts/boards/models/list.js182
-rw-r--r--app/assets/javascripts/boards/models/milestone.js15
-rw-r--r--app/assets/javascripts/boards/models/project.js7
-rw-r--r--app/assets/javascripts/boards/mount_multiple_boards_switcher.js15
-rw-r--r--app/assets/javascripts/boards/stores/actions.js74
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js883
-rw-r--r--app/assets/javascripts/boards/stores/boards_store_ee.js5
-rw-r--r--app/assets/javascripts/boards/stores/getters.js6
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js4
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
-rw-r--r--app/assets/javascripts/boards/stores/state.js2
-rw-r--r--app/assets/javascripts/captcha/init_recaptcha_script.js2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue2
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js8
-rw-r--r--app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue3
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue2
-rw-r--r--app/assets/javascripts/commit/image_file.js7
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js9
-rw-r--r--app/assets/javascripts/commit/pipelines/utils.js11
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue37
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/image.vue1
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue142
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue23
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue23
-rw-r--r--app/assets/javascripts/content_editor/constants.js4
-rw-r--r--app/assets/javascripts/content_editor/content_editor.stories.js27
-rw-r--r--app/assets/javascripts/content_editor/extensions/audio.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/blockquote.js34
-rw-r--r--app/assets/javascripts/content_editor/extensions/bullet_list.js20
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/description_item.js49
-rw-r--r--app/assets/javascripts/content_editor/extensions/description_list.js23
-rw-r--r--app/assets/javascripts/content_editor/extensions/division.js17
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/figure.js16
-rw-r--r--app/assets/javascripts/content_editor/extensions/figure_caption.js16
-rw-r--r--app/assets/javascripts/content_editor/extensions/html_marks.js66
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js23
-rw-r--r--app/assets/javascripts/content_editor/extensions/inline_diff.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js16
-rw-r--r--app/assets/javascripts/content_editor/extensions/ordered_list.js16
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js66
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js41
-rw-r--r--app/assets/javascripts/content_editor/extensions/subscript.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/superscript.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js8
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_item.js6
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_list.js28
-rw-r--r--app/assets/javascripts/content_editor/extensions/video.js10
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js16
-rw-r--r--app/assets/javascripts/content_editor/services/feature_flags.js3
-rw-r--r--app/assets/javascripts/content_editor/services/mark_utils.js17
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js163
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_sourcemap.js40
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js345
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue74
-rw-r--r--app/assets/javascripts/create_cluster/eks_cluster/constants.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/components/banner.vue54
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue57
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue13
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue47
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js14
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/getters.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js18
-rw-r--r--app/assets/javascripts/cycle_analytics/store/state.js3
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue63
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js16
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutations.js33
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js2
-rw-r--r--app/assets/javascripts/deprecated_notes.js (renamed from app/assets/javascripts/notes.js)8
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_scaler.vue18
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue2
-rw-r--r--app/assets/javascripts/design_management/components/image.vue22
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql7
-rw-r--r--app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql6
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue12
-rw-r--r--app/assets/javascripts/design_management/pages/index.vue3
-rw-r--r--app/assets/javascripts/design_management/utils/design_management_utils.js7
-rw-r--r--app/assets/javascripts/design_management/utils/error_messages.js1
-rw-r--r--app/assets/javascripts/diffs/components/app.vue113
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue10
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue5
-rw-r--r--app/assets/javascripts/diffs/components/pre_renderer.vue1
-rw-r--r--app/assets/javascripts/diffs/constants.js5
-rw-r--r--app/assets/javascripts/diffs/index.js96
-rw-r--r--app/assets/javascripts/diffs/store/actions.js22
-rw-r--r--app/assets/javascripts/diffs/store/getters.js3
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js6
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js4
-rw-r--r--app/assets/javascripts/diffs/utils/preferences.js13
-rw-r--r--app/assets/javascripts/dropzone_input.js8
-rw-r--r--app/assets/javascripts/due_date_select.js191
-rw-r--r--app/assets/javascripts/emoji/index.js5
-rw-r--r--app/assets/javascripts/emoji/support/unicode_support_map.js2
-rw-r--r--app/assets/javascripts/environments/components/container.vue6
-rw-r--r--app/assets/javascripts/environments/components/edit_environment.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue15
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue50
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue41
-rw-r--r--app/assets/javascripts/environments/components/environments_detail_header.vue8
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue7
-rw-r--r--app/assets/javascripts/environments/constants.js2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js3
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue5
-rw-r--r--app/assets/javascripts/environments/index.js4
-rw-r--r--app/assets/javascripts/environments/mount_show.js1
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue3
-rw-r--r--app/assets/javascripts/error_tracking/store/list/mutations.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/components/app.vue79
-rw-r--r--app/assets/javascripts/error_tracking_settings/index.js11
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js4
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/mutations.js14
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/state.js1
-rw-r--r--app/assets/javascripts/error_tracking_settings/utils.js10
-rw-r--r--app/assets/javascripts/experimentation/utils.js25
-rw-r--r--app/assets/javascripts/feature_flags/components/edit_feature_flag.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue2
-rw-r--r--app/assets/javascripts/filtered_search/services/recent_searches_service.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue4
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js2
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js4
-rw-r--r--app/assets/javascripts/groups/components/app.vue2
-rw-r--r--app/assets/javascripts/groups/components/groups.vue8
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue18
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue11
-rw-r--r--app/assets/javascripts/groups/init_invite_members_banner.js13
-rw-r--r--app/assets/javascripts/header_search/components/app.vue83
-rw-r--r--app/assets/javascripts/header_search/components/header_search_default_items.vue42
-rw-r--r--app/assets/javascripts/header_search/components/header_search_scoped_items.vue31
-rw-r--r--app/assets/javascripts/header_search/constants.js17
-rw-r--r--app/assets/javascripts/header_search/index.js26
-rw-r--r--app/assets/javascripts/header_search/store/actions.js5
-rw-r--r--app/assets/javascripts/header_search/store/getters.js135
-rw-r--r--app/assets/javascripts/header_search/store/index.js18
-rw-r--r--app/assets/javascripts/header_search/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/header_search/store/mutations.js7
-rw-r--r--app/assets/javascripts/header_search/store/state.js8
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue3
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue8
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue3
-rw-r--r--app/assets/javascripts/ide/services/terminals.js4
-rw-r--r--app/assets/javascripts/ide/utils.js5
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue69
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue53
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue76
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue57
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js89
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql18
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js24
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql13
-rw-r--r--app/assets/javascripts/import_entities/import_groups/utils.js9
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js17
-rw-r--r--app/assets/javascripts/incidents/list.js2
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js12
-rw-r--r--app/assets/javascripts/init_deprecated_notes.js (renamed from app/assets/javascripts/init_notes.js)2
-rw-r--r--app/assets/javascripts/init_diff_stats_dropdown.js30
-rw-r--r--app/assets/javascripts/init_issuable_sidebar.js10
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue4
-rw-r--r--app/assets/javascripts/invite_members/components/import_a_project_modal.vue157
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/project_select.vue143
-rw-r--r--app/assets/javascripts/invite_members/init_import_a_project_modal.js23
-rw-r--r--app/assets/javascripts/issuable/components/csv_export_modal.vue4
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_item.vue9
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue19
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue3
-rw-r--r--app/assets/javascripts/issue_show/components/locked_warning.vue6
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue131
-rw-r--r--app/assets/javascripts/issues_list/constants.js22
-rw-r--r--app/assets/javascripts/issues_list/index.js22
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql35
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql30
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql105
-rw-r--r--app/assets/javascripts/issues_list/queries/issue.fragment.graphql1
-rw-r--r--app/assets/javascripts/issues_list/queries/iteration.fragment.graphql4
-rw-r--r--app/assets/javascripts/issues_list/queries/label.fragment.graphql6
-rw-r--r--app/assets/javascripts/issues_list/queries/milestone.fragment.graphql4
-rw-r--r--app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql8
-rw-r--r--app/assets/javascripts/issues_list/queries/search_iterations.query.graphql18
-rw-r--r--app/assets/javascripts/issues_list/queries/search_labels.query.graphql18
-rw-r--r--app/assets/javascripts/issues_list/queries/search_milestones.query.graphql16
-rw-r--r--app/assets/javascripts/issues_list/queries/search_users.query.graphql20
-rw-r--r--app/assets/javascripts/issues_list/queries/user.fragment.graphql6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/utils.js4
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue8
-rw-r--r--app/assets/javascripts/jobs/components/table/cells/actions_cell.vue183
-rw-r--r--app/assets/javascripts/jobs/components/table/constants.js23
-rw-r--r--app/assets/javascripts/jobs/components/table/event_hub.js3
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql3
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql10
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql10
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql10
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql10
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/jobs/components/table/index.js5
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue2
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue10
-rw-r--r--app/assets/javascripts/labels_select.js52
-rw-r--r--app/assets/javascripts/learn_gitlab/track_learn_gitlab.js10
-rw-r--r--app/assets/javascripts/lib/apollo/instrumentation_link.js29
-rw-r--r--app/assets/javascripts/lib/dompurify.js6
-rw-r--r--app/assets/javascripts/lib/graphql.js20
-rw-r--r--app/assets/javascripts/lib/logger/hello.js16
-rw-r--r--app/assets/javascripts/lib/logger/hello_deferred.js5
-rw-r--r--app/assets/javascripts/lib/logger/index.js6
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js28
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js11
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js105
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js12
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js9
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js37
-rw-r--r--app/assets/javascripts/main.js37
-rw-r--r--app/assets/javascripts/main_jh.js1
-rw-r--r--app/assets/javascripts/merge_request.js29
-rw-r--r--app/assets/javascripts/merge_request_tabs.js71
-rw-r--r--app/assets/javascripts/milestone_select.js118
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue3
-rw-r--r--app/assets/javascripts/milestones/stores/mutations.js4
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue3
-rw-r--r--app/assets/javascripts/mr_notes/index.js36
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue16
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue9
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue85
-rw-r--r--app/assets/javascripts/notes/components/comment_type_dropdown.vue114
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue7
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue6
-rw-r--r--app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions/reply_button.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue7
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js1
-rw-r--r--app/assets/javascripts/packages/details/components/package_history.vue1
-rw-r--r--app/assets/javascripts/packages/shared/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue105
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue55
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue32
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue42
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue46
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue34
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue57
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue47
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue129
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue132
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.js16
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue2
-rw-r--r--app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue2
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js34
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/new/toggle_invite_members.js14
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/compare/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue (renamed from app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue)0
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue116
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/index/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js13
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue98
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js40
-rw-r--r--app/assets/javascripts/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql14
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/show/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue2
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/usage_quotas/index.js23
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/work_items/index/index.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue8
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue12
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue9
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue18
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue12
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue9
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue11
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue23
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue7
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue5
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql11
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js9
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js15
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue61
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue14
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/graph/accessors.js25
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue20
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js78
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js52
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue71
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js60
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_graph.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_test_details.js34
-rw-r--r--app/assets/javascripts/pipelines/utils.js52
-rw-r--r--app/assets/javascripts/popovers/components/popovers.vue21
-rw-r--r--app/assets/javascripts/project_select_combo_button.js2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue1
-rw-r--r--app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql1
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js7
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue12
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js2
-rw-r--r--app/assets/javascripts/projects/project_new.js22
-rw-r--r--app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue5
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue54
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/app.vue106
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_table.vue78
-rw-r--r--app/assets/javascripts/projects/storage_counter/constants.js61
-rw-r--r--app/assets/javascripts/projects/storage_counter/index.js51
-rw-r--r--app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql16
-rw-r--r--app/assets/javascripts/projects/storage_counter/utils.js40
-rw-r--r--app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue53
-rw-r--r--app/assets/javascripts/projects/terraform_notification/constants.js3
-rw-r--r--app/assets/javascripts/projects/terraform_notification/index.js14
-rw-r--r--app/assets/javascripts/prometheus_alerts/components/reset_key.vue3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js2
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue3
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue6
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js16
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue6
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue6
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue19
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js6
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue10
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue7
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue7
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue21
-rw-r--r--app/assets/javascripts/repository/constants.js2
-rw-r--r--app/assets/javascripts/repository/mixins/preload.js3
-rw-r--r--app/assets/javascripts/repository/pages/blob.vue15
-rw-r--r--app/assets/javascripts/rest_api.js2
-rw-r--r--app/assets/javascripts/right_sidebar.js7
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue32
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue88
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue2
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/status_token_config.js32
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue1
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token_config.js12
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/type_token_config.js20
-rw-r--r--app/assets/javascripts/runner/constants.js6
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners.query.graphql35
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue137
-rw-r--r--app/assets/javascripts/runner/group_runners/index.js11
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js1
-rw-r--r--app/assets/javascripts/search/highlight_blob_search_result.js2
-rw-r--r--app/assets/javascripts/search/store/actions.js2
-rw-r--r--app/assets/javascripts/search/store/utils.js4
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js15
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js1
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue3
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js58
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js5
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js6
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js17
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js14
-rw-r--r--app/assets/javascripts/sidebar/track_invite_members.js6
-rw-r--r--app/assets/javascripts/snippet/snippet_show.js4
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue6
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js2
-rw-r--r--app/assets/javascripts/tracking/constants.js4
-rw-r--r--app/assets/javascripts/tracking/index.js7
-rw-r--r--app/assets/javascripts/tracking/tracking.js40
-rw-r--r--app/assets/javascripts/tracking/utils.js24
-rw-r--r--app/assets/javascripts/user_popovers.js4
-rw-r--r--app/assets/javascripts/users_select/index.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue75
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/terraform/terraform_plan.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue159
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js17
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js11
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue15
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/title_area.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.stories.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/settings/settings_block.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue42
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue132
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue145
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue40
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue83
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue278
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js52
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js50
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js22
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js38
-rw-r--r--app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue148
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue23
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js2
-rw-r--r--app/assets/javascripts/webpack_non_compiled_placeholder.js22
-rw-r--r--app/assets/javascripts/whats_new/components/feature.vue6
-rw-r--r--app/assets/javascripts/work_items/components/app.vue9
-rw-r--r--app/assets/javascripts/work_items/graphql/typedefs.graphql0
-rw-r--r--app/assets/javascripts/work_items/index.js13
-rw-r--r--app/assets/stylesheets/application_dark.scss11
-rw-r--r--app/assets/stylesheets/components/content_editor.scss20
-rw-r--r--app/assets/stylesheets/errors.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss8
-rw-r--r--app/assets/stylesheets/framework/diffs.scss43
-rw-r--r--app/assets/stylesheets/framework/filters.scss9
-rw-r--r--app/assets/stylesheets/framework/icons.scss1
-rw-r--r--app/assets/stylesheets/framework/typography.scss21
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/mailer.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss12
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/cycle_analytics.scss289
-rw-r--r--app/assets/stylesheets/page_bundles/escalation_policies.scss25
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/new_namespace.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss310
-rw-r--r--app/assets/stylesheets/page_bundles/reports.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/signup.scss30
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss5
-rw-r--r--app/assets/stylesheets/pages/login.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/pages/profile.scss8
-rw-r--r--app/assets/stylesheets/pages/search.scss19
-rw-r--r--app/assets/stylesheets/pages/tree.scss1
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss85
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss35
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss10
-rw-r--r--app/assets/stylesheets/themes/_dark.scss28
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss30
-rw-r--r--app/assets/stylesheets/themes/theme_light.scss10
-rw-r--r--app/assets/stylesheets/utilities.scss25
589 files changed, 8225 insertions, 8519 deletions
diff --git a/app/assets/images/learn_gitlab/code_owners_enabled.svg b/app/assets/images/learn_gitlab/code_owners_enabled.svg
deleted file mode 100644
index 019d74c64cc..00000000000
--- a/app/assets/images/learn_gitlab/code_owners_enabled.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M13 25C19.6274 25 25 19.6274 25 13C25 6.37258 19.6274 1 13 1C6.37258 1 1 6.37258 1 13C1 19.6274 6.37258 25 13 25Z" fill="white" stroke="#C2B7E6" stroke-width="2"/>
-<path d="M1.16748 12.3359C2.88075 11.7701 4.4618 10.8635 5.81545 9.67055C7.16911 8.47763 8.26738 7.02313 9.04415 5.39461M6.94481 2.60461C9.28681 6.43995 13.5115 8.99995 18.3335 8.99995C20.2715 8.99995 22.1135 8.58661 23.7748 7.84261L6.94481 2.60461Z" stroke="#C2B7E6"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1176 15.8941H15.7647C15.7647 17.447 14.4941 18.7176 12.9412 18.7176C11.3882 18.7176 10.1176 17.447 10.1176 15.8941ZM9.05882 15.1882C8.47294 15.1882 8 14.7153 8 14.1294C8 13.5435 8.47294 13.0706 9.05882 13.0706C9.64471 13.0706 10.1176 13.5435 10.1176 14.1294C10.1176 14.7153 9.64471 15.1882 9.05882 15.1882ZM16.8235 15.1882C16.2376 15.1882 15.7647 14.7153 15.7647 14.1294C15.7647 13.5435 16.2376 13.0706 16.8235 13.0706C17.4094 13.0706 17.8824 13.5435 17.8824 14.1294C17.8824 14.7153 17.4094 15.1882 16.8235 15.1882Z" fill="#6B4FBB"/>
-</svg>
diff --git a/app/assets/images/learn_gitlab/git_write.svg b/app/assets/images/learn_gitlab/git_write.svg
deleted file mode 100644
index ad87b3f3b12..00000000000
--- a/app/assets/images/learn_gitlab/git_write.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-<svg width="40" height="39" viewBox="0 0 40 39" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0)">
-<path d="M32.2886 3.99573H37.8624C38.1834 3.99573 38.4435 4.25587 38.4435 4.57679V33.9598C38.4435 34.2808 38.1834 34.5409 37.8624 34.5409H32.2886V3.99573Z" fill="#F0F0F0" stroke="#DBDBDB" stroke-width="2"/>
-<path d="M10.757 9.4011L10.7363 4.92686C10.7337 4.35386 11.1491 3.86447 11.7148 3.77395L30.908 0.703095C31.614 0.590124 32.2537 1.13556 32.2537 1.85062V37.0106C32.2537 37.723 31.6184 38.2678 30.9143 38.1591L11.8555 35.2171C11.2908 35.13 10.8733 34.6453 10.8707 34.074L10.8502 29.6368" stroke="#DBDBDB" stroke-width="2"/>
-<path d="M11.2195 29.7561C16.877 29.7561 21.4634 25.1698 21.4634 19.5122C21.4634 13.8547 16.877 9.26831 11.2195 9.26831C5.56194 9.26831 0.975586 13.8547 0.975586 19.5122C0.975586 25.1698 5.56194 29.7561 11.2195 29.7561Z" stroke="#6E49CB"/>
-<path d="M11.2194 27.8048C15.7994 27.8048 19.5121 24.0921 19.5121 19.5122C19.5121 14.9322 15.7994 11.2195 11.2194 11.2195C6.63952 11.2195 2.92676 14.9322 2.92676 19.5122C2.92676 24.0921 6.63952 27.8048 11.2194 27.8048Z" fill="#6E49CB"/>
-<path d="M11.2194 27.8048C15.7994 27.8048 19.5121 24.0921 19.5121 19.5122C19.5121 14.9322 15.7994 11.2195 11.2194 11.2195C6.63952 11.2195 2.92676 14.9322 2.92676 19.5122C2.92676 24.0921 6.63952 27.8048 11.2194 27.8048Z" fill="white" fill-opacity="0.9"/>
-<path d="M10.8843 23.4146V16.276" stroke="#6E49CB" stroke-linecap="round"/>
-<path d="M7.31689 19.6609H14.634" stroke="#6E49CB" stroke-linecap="round"/>
-</g>
-<defs>
-<clipPath id="clip0">
-<rect width="40" height="39.0244" fill="white"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/app/assets/images/learn_gitlab/issue_created.svg b/app/assets/images/learn_gitlab/issue_created.svg
deleted file mode 100644
index 01652b97fc0..00000000000
--- a/app/assets/images/learn_gitlab/issue_created.svg
+++ /dev/null
@@ -1,65 +0,0 @@
-<svg width="81" height="48" viewBox="0 0 81 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0)">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M42.9799 11.6386C42.9688 11.7501 42.955 11.8786 42.9384 12.0222C42.8865 12.4687 42.8257 12.9142 42.756 13.3582C42.7493 13.3954 42.7501 13.4335 42.7584 13.4704C42.7667 13.5072 42.7822 13.542 42.8041 13.5728C42.826 13.6035 42.8538 13.6296 42.8859 13.6495C42.918 13.6693 42.9538 13.6826 42.9911 13.6884C43.0284 13.6943 43.0664 13.6926 43.1031 13.6835C43.1397 13.6745 43.1742 13.6582 43.2045 13.6356C43.2347 13.613 43.2602 13.5847 43.2793 13.5521C43.2985 13.5196 43.3109 13.4835 43.316 13.4461C43.3915 12.9637 43.4531 12.506 43.5015 12.0874C43.5221 11.9092 43.5408 11.7308 43.5577 11.5522C43.5609 11.5151 43.5568 11.4777 43.5456 11.4422C43.5343 11.4067 43.5162 11.3738 43.4923 11.3453C43.4684 11.3168 43.439 11.2933 43.406 11.2761C43.373 11.2589 43.3369 11.2484 43.2998 11.2452C43.2627 11.242 43.2253 11.2461 43.1898 11.2573C43.1543 11.2685 43.1214 11.2866 43.0929 11.3106C43.0644 11.3345 43.0409 11.3638 43.0237 11.3969C43.0065 11.4299 42.996 11.466 42.9928 11.5031C42.9909 11.5263 42.9866 11.572 42.9799 11.6386ZM41.9287 16.9968C41.9171 17.0322 41.9127 17.0695 41.9156 17.1066C41.9185 17.1438 41.9287 17.1799 41.9456 17.2131C41.9624 17.2463 41.9857 17.2758 42.014 17.3C42.0423 17.3242 42.0751 17.3426 42.1105 17.3542C42.1459 17.3657 42.1832 17.3701 42.2203 17.3672C42.2574 17.3643 42.2936 17.3541 42.3268 17.3373C42.36 17.3204 42.3895 17.2971 42.4137 17.2688C42.4379 17.2405 42.4563 17.2078 42.4678 17.1724C42.6521 16.606 42.8172 15.985 42.9645 15.321C42.9734 15.2843 42.975 15.2462 42.9691 15.2089C42.9631 15.1716 42.9498 15.1359 42.9299 15.1039C42.91 15.0718 42.8839 15.044 42.8531 15.0222C42.8223 15.0004 42.7874 14.9849 42.7506 14.9767C42.7137 14.9686 42.6756 14.9678 42.6385 14.9746C42.6013 14.9813 42.5659 14.9954 42.5343 15.016C42.5027 15.0367 42.4755 15.0634 42.4543 15.0947C42.4332 15.1259 42.4185 15.1611 42.4111 15.1981C42.2675 15.8456 42.1069 16.4491 41.9287 16.9968ZM40.0489 19.874C40.0136 19.8862 39.981 19.9053 39.9531 19.9302C39.9252 19.955 39.9025 19.9852 39.8863 20.0189C39.8701 20.0526 39.8607 20.0891 39.8587 20.1265C39.8566 20.1638 39.862 20.2012 39.8745 20.2364C39.887 20.2716 39.9063 20.3041 39.9313 20.3318C39.9564 20.3596 39.9867 20.3821 40.0205 20.3981C40.0543 20.4141 40.0909 20.4232 40.1283 20.425C40.1656 20.4267 40.2029 20.4211 40.2381 20.4084C40.8012 20.209 41.2854 19.713 41.7083 18.9671C41.7454 18.9017 41.755 18.8242 41.735 18.7517C41.715 18.6792 41.667 18.6177 41.6016 18.5806C41.5362 18.5435 41.4587 18.5339 41.3862 18.554C41.3137 18.574 41.2522 18.622 41.2151 18.6874C40.8532 19.3257 40.4601 19.7283 40.0489 19.874ZM36.3662 20.7087C36.3319 20.7231 36.3007 20.7442 36.2746 20.7706C36.2484 20.7971 36.2277 20.8285 36.2136 20.863C36.1996 20.8974 36.1925 20.9343 36.1927 20.9716C36.1929 21.0088 36.2004 21.0456 36.2149 21.0799C36.2293 21.1142 36.2504 21.1454 36.2769 21.1715C36.3033 21.1977 36.3347 21.2184 36.3692 21.2324C36.4037 21.2465 36.4406 21.2536 36.4778 21.2534C36.515 21.2532 36.5518 21.2456 36.5861 21.2312C37.1757 20.9829 37.7714 20.792 38.3357 20.6679C38.4091 20.6517 38.4731 20.607 38.5136 20.5437C38.5541 20.4803 38.5677 20.4035 38.5516 20.3301C38.5354 20.2566 38.4907 20.1926 38.4274 20.1521C38.364 20.1117 38.2872 20.098 38.2138 20.1142C37.6153 20.2459 36.9871 20.4473 36.3662 20.7087ZM33.1143 22.7955C33.0607 22.8475 33.0298 22.9186 33.0283 22.9932C33.0267 23.0678 33.0547 23.1401 33.1061 23.1942C33.1576 23.2483 33.2282 23.28 33.3029 23.2823C33.3775 23.2846 33.45 23.2574 33.5047 23.2066C33.9359 22.7967 34.4242 22.42 34.9553 22.0829C35.0187 22.0426 35.0636 21.9788 35.08 21.9054C35.0964 21.832 35.083 21.7551 35.0427 21.6916C35.0024 21.6282 34.9385 21.5833 34.8651 21.5669C34.7918 21.5505 34.7149 21.5639 34.6514 21.6042C34.0901 21.9605 33.5729 22.3598 33.1143 22.7955ZM31.0777 26.1259C31.0686 26.162 31.0667 26.1995 31.0721 26.2364C31.0776 26.2732 31.0902 26.3086 31.1093 26.3406C31.1284 26.3725 31.1536 26.4004 31.1835 26.4226C31.2134 26.4448 31.2474 26.4609 31.2835 26.47C31.3196 26.4791 31.3571 26.481 31.3939 26.4755C31.4308 26.4701 31.4662 26.4575 31.4981 26.4384C31.5301 26.4193 31.558 26.394 31.5802 26.3642C31.6024 26.3343 31.6185 26.3003 31.6276 26.2642C31.7727 25.6871 32.0133 25.1347 32.3419 24.611C32.3799 24.5474 32.3915 24.4715 32.3742 24.3994C32.3569 24.3274 32.3121 24.265 32.2493 24.2256C32.1866 24.1862 32.1109 24.1729 32.0385 24.1886C31.9661 24.2043 31.9027 24.2478 31.8619 24.3096C31.5023 24.8826 31.2379 25.4896 31.0777 26.1259ZM31.0276 29.9893C31.0322 30.0262 31.0441 30.0619 31.0626 30.0943C31.081 30.1266 31.1056 30.155 31.135 30.1778C31.1644 30.2007 31.1981 30.2175 31.234 30.2273C31.2699 30.2371 31.3074 30.2398 31.3443 30.2352C31.3813 30.2305 31.4169 30.2186 31.4493 30.2002C31.4816 30.1818 31.51 30.1571 31.5328 30.1277C31.5557 30.0983 31.5725 30.0647 31.5823 30.0288C31.5922 29.9929 31.5948 29.9554 31.5902 29.9184C31.5208 29.3685 31.4806 28.7587 31.4683 28.0633C31.467 27.9881 31.4358 27.9165 31.3817 27.8643C31.3276 27.8121 31.255 27.7835 31.1798 27.7848C31.1046 27.7861 31.0331 27.8173 30.9808 27.8714C30.9286 27.9255 30.9 27.9981 30.9014 28.0733C30.914 28.7882 30.9556 29.418 31.0276 29.9893ZM32.2028 33.6691C32.238 33.7355 32.2981 33.7853 32.37 33.8074C32.4418 33.8295 32.5195 33.8222 32.586 33.7871C32.6524 33.7519 32.7022 33.6918 32.7243 33.6199C32.7465 33.5481 32.7392 33.4704 32.704 33.4039C32.3876 32.8064 32.1554 32.271 31.9768 31.7139C31.9539 31.6423 31.9034 31.5828 31.8365 31.5484C31.7697 31.514 31.6919 31.5075 31.6203 31.5305C31.5487 31.5535 31.4892 31.6039 31.4548 31.6708C31.4204 31.7376 31.414 31.8154 31.4369 31.887C31.6265 32.4779 31.8719 33.0435 32.2028 33.6691ZM33.6326 36.0823C33.8058 36.3634 33.9778 36.6453 34.1485 36.9279C34.1874 36.9923 34.2502 37.0386 34.3233 37.0567C34.3963 37.0747 34.4734 37.063 34.5378 37.0241C34.6022 36.9853 34.6485 36.9224 34.6665 36.8494C34.6846 36.7764 34.6729 36.6992 34.634 36.6348C34.4623 36.351 34.2895 36.0678 34.1155 35.7854C34.1306 35.81 33.7462 35.1858 33.6457 35.0219C33.6264 34.9897 33.601 34.9617 33.5709 34.9394C33.5407 34.9171 33.5065 34.901 33.4701 34.892C33.4337 34.8831 33.3958 34.8814 33.3588 34.8872C33.3217 34.8929 33.2862 34.906 33.2542 34.9256C33.2222 34.9451 33.1945 34.9709 33.1725 35.0013C33.1506 35.0317 33.1349 35.0661 33.1263 35.1026C33.1177 35.1391 33.1165 35.177 33.1227 35.2139C33.1289 35.2509 33.1423 35.2863 33.1623 35.3181C33.2632 35.4825 33.6481 36.1076 33.6326 36.0825V36.0823ZM35.3062 40.3242C35.3067 40.3615 35.3145 40.3982 35.3292 40.4324C35.3439 40.4666 35.3652 40.4976 35.3918 40.5236C35.4185 40.5496 35.45 40.57 35.4846 40.5838C35.5192 40.5976 35.5561 40.6045 35.5933 40.604C35.6306 40.6035 35.6673 40.5957 35.7015 40.581C35.7357 40.5663 35.7667 40.545 35.7927 40.5184C35.8187 40.4917 35.8392 40.4602 35.853 40.4256C35.8668 40.391 35.8736 40.3541 35.8731 40.3169C35.8648 39.6518 35.7475 38.9926 35.526 38.3655C35.4998 38.2962 35.4474 38.2399 35.3802 38.2087C35.313 38.1775 35.2363 38.1738 35.1664 38.1985C35.0966 38.2232 35.0392 38.2743 35.0065 38.3407C34.9738 38.4072 34.9684 38.4839 34.9915 38.5543C35.1924 39.1231 35.2987 39.721 35.3062 40.3242ZM34.27 43.7311C34.2299 43.7937 34.216 43.8695 34.2313 43.9422C34.2466 44.015 34.2898 44.0788 34.3517 44.12C34.4135 44.1612 34.4891 44.1764 34.5621 44.1624C34.6351 44.1484 34.6997 44.1063 34.7419 44.0452C35.109 43.4961 35.3947 42.8967 35.59 42.2658C35.6121 42.1939 35.6048 42.1162 35.5696 42.0497C35.5344 41.9833 35.4743 41.9335 35.4024 41.9114C35.3305 41.8893 35.2528 41.8967 35.1864 41.9319C35.1199 41.9671 35.0702 42.0272 35.0481 42.0991C34.8689 42.6778 34.6068 43.2275 34.27 43.7311Z" fill="#FDE5D8"/>
-<path d="M60.8446 31.6803L53.1713 38.2926M49.7697 32.4009L59.3328 29.0618L49.7697 32.4009Z" stroke="#FDE5D8" stroke-linecap="round"/>
-<path d="M60.2631 30.4887C60.3987 30.4104 60.4451 30.237 60.3668 30.1014C60.2886 29.9659 60.1152 29.9194 59.9796 29.9977L54.1288 33.3756C53.9932 33.4539 53.9468 33.6273 54.0251 33.7629C54.1033 33.8984 54.2767 33.9449 54.4123 33.8666L60.2631 30.4887Z" fill="#FDE5D8"/>
-<path d="M63.2421 30.9507L61.2578 27.5138C61.1535 27.3331 60.9223 27.2711 60.7415 27.3755L59.9327 27.8425C59.752 27.9468 59.69 28.178 59.7944 28.3588L61.7786 31.7956C61.883 31.9764 62.1142 32.0383 62.2949 31.9339L63.1037 31.467C63.2845 31.3626 63.3464 31.1314 63.2421 30.9507Z" fill="white" stroke="#FDE5D8"/>
-<path d="M69.8124 20.1746L73.8754 27.2119L63.7936 33.0326L59.7306 25.9953L69.8124 20.1746Z" stroke="#FDE5D8"/>
-<path d="M68.6454 29.795L68.3599 32.8374C68.35 32.9411 68.2766 33.0767 68.1935 33.1419L64.4623 36.0762C64.2994 36.2045 64.2327 36.1536 64.3133 35.9633L65.268 33.7089L65.5559 31.5788" stroke="#FDE5D8"/>
-<path d="M64.9604 23.4123L62.1829 22.1385C62.088 22.0951 61.934 22.0911 61.836 22.1302L57.4292 23.8944C57.2367 23.9715 57.2473 24.0546 57.4525 24.08L59.8823 24.3805L61.8709 25.1962" stroke="#FDE5D8"/>
-<path d="M69.8126 20.1746L76.321 20.7344C76.6326 20.7612 76.778 21.0118 76.645 21.2955L73.8758 27.2118" stroke="#FDE5D8"/>
-<path d="M62.0503 27.3839C62.1285 27.5195 62.3012 27.5663 62.4359 27.4886C62.5705 27.4108 62.6163 27.2379 62.538 27.1023C62.4597 26.9667 62.2871 26.9198 62.1524 26.9976C62.0177 27.0754 61.972 27.2483 62.0503 27.3839Z" fill="#FC8A51"/>
-<path d="M62.6173 28.3658C62.6955 28.5014 62.8682 28.5483 63.0028 28.4705C63.1375 28.3928 63.1832 28.2198 63.105 28.0842C63.0267 27.9487 62.8541 27.9018 62.7194 27.9795C62.5847 28.0573 62.539 28.2302 62.6173 28.3658Z" fill="#FC8A51"/>
-<path d="M63.1841 29.3476C63.2624 29.4832 63.435 29.5301 63.5697 29.4523C63.7044 29.3746 63.7501 29.2016 63.6718 29.066C63.5935 28.9305 63.4209 28.8836 63.2862 28.9613C63.1516 29.0391 63.1058 29.212 63.1841 29.3476Z" fill="#FC8A51"/>
-<path d="M63.7511 30.3297C63.8294 30.4653 64.002 30.5121 64.1367 30.4344C64.2714 30.3566 64.3171 30.1837 64.2388 30.0481C64.1605 29.9125 63.9879 29.8656 63.8532 29.9434C63.7186 30.0212 63.6728 30.1941 63.7511 30.3297Z" fill="#FC8A51"/>
-<path d="M65.9899 27.0731C66.5378 28.0221 67.7464 28.3501 68.6894 27.8057C69.6324 27.2612 69.9527 26.0505 69.4048 25.1015C68.8568 24.1524 67.6482 23.8244 66.7052 24.3689C65.7622 24.9133 65.4419 26.124 65.9899 27.0731Z" stroke="#FDE5D8"/>
-<path d="M66.8032 26.6036C67.0902 27.1008 67.7233 27.2726 68.2173 26.9874C68.7113 26.7022 68.879 26.068 68.592 25.5709C68.305 25.0738 67.6719 24.902 67.1779 25.1872C66.684 25.4723 66.5162 26.1065 66.8032 26.6036Z" stroke="#FDE5D8"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M18.5279 7.40988C18.4991 7.72887 18.5432 8.13101 18.7163 8.57152C18.7364 8.61656 18.7731 8.65211 18.8188 8.6708C18.8645 8.68948 18.9156 8.68987 18.9615 8.67188C19.0075 8.65389 19.0447 8.61891 19.0656 8.57418C19.0864 8.52945 19.0892 8.47842 19.0734 8.43167C18.924 8.05108 18.8863 7.70752 18.91 7.44408C18.9119 7.42254 18.9137 7.40893 18.9145 7.40421C18.9193 7.37911 18.9189 7.35331 18.9136 7.32834C18.9082 7.30336 18.8979 7.2797 18.8833 7.25876C18.8687 7.23781 18.85 7.22 18.8284 7.20637C18.8068 7.19273 18.7827 7.18355 18.7575 7.17936C18.7323 7.17517 18.7065 7.17605 18.6817 7.18196C18.6568 7.18787 18.6334 7.19868 18.6128 7.21376C18.5922 7.22885 18.5748 7.2479 18.5616 7.26979C18.5485 7.29169 18.5398 7.31599 18.5362 7.34128C18.5326 7.36404 18.5298 7.38692 18.5279 7.40988ZM20.6986 10.3736C20.7914 10.4189 20.9061 10.3855 20.9547 10.2991C21.0033 10.213 20.9675 10.1064 20.8746 10.061C20.5219 9.88963 20.2211 9.69933 19.9686 9.49278C19.9299 9.46178 19.8811 9.44626 19.8316 9.44923C19.7822 9.4522 19.7355 9.47344 19.7008 9.50884C19.6845 9.52574 19.6719 9.5459 19.6638 9.568C19.6557 9.59009 19.6524 9.61364 19.654 9.63711C19.6556 9.66058 19.6622 9.68344 19.6732 9.70422C19.6842 9.725 19.6995 9.74323 19.718 9.75772C19.9936 9.98298 20.3195 10.1892 20.6988 10.3738L20.6986 10.3736ZM23.1801 11.5188C23.2048 11.5264 23.2308 11.5289 23.2565 11.5263C23.2822 11.5238 23.3072 11.5161 23.3299 11.5038C23.3527 11.4915 23.3727 11.4748 23.389 11.4546C23.4052 11.4345 23.4172 11.4113 23.4244 11.3865C23.4399 11.3362 23.435 11.2819 23.4107 11.2352C23.3865 11.1886 23.3448 11.1533 23.2948 11.1371C22.9212 11.0216 22.5486 10.903 22.177 10.7814C22.1524 10.7735 22.1264 10.7705 22.1007 10.7727C22.0749 10.7749 22.0498 10.7822 22.0269 10.7941C22.004 10.8061 21.9837 10.8225 21.9672 10.8425C21.9507 10.8624 21.9383 10.8854 21.9308 10.9101C21.9146 10.9602 21.9187 11.0146 21.9423 11.0616C21.966 11.1087 22.0072 11.1445 22.057 11.1614C22.3837 11.269 22.606 11.3391 23.1801 11.5188ZM25.4353 12.2611C25.5204 12.3076 25.6285 12.2796 25.6772 12.1987C25.6887 12.1795 25.6962 12.158 25.6991 12.1358C25.7021 12.1135 25.7004 12.0909 25.6943 12.0692C25.6882 12.0476 25.6777 12.0275 25.6635 12.0101C25.6493 11.9927 25.6318 11.9784 25.6118 11.968C25.3295 11.8132 25.0086 11.676 24.6165 11.5381C24.5246 11.5059 24.4228 11.5507 24.389 11.6382C24.3549 11.7259 24.4022 11.823 24.4938 11.8554C24.8688 11.9871 25.1723 12.1169 25.4353 12.2611ZM27.2083 14.2457C27.247 14.3391 27.3614 14.3859 27.4632 14.3504C27.5653 14.3149 27.6163 14.2106 27.5776 14.1172C27.4169 13.7302 27.2274 13.3999 27.0036 13.1147C26.94 13.0336 26.8166 13.0151 26.7279 13.0735C26.6393 13.1317 26.6193 13.2447 26.6829 13.3258C26.8859 13.5843 27.0594 13.887 27.2083 14.2457ZM27.7057 16.8237C27.7091 16.9282 27.813 17.0108 27.9378 17.008C28.0625 17.005 28.1607 16.9178 28.1575 16.8133C28.1437 16.3968 28.1065 16.0185 28.04 15.6568C28.0209 15.5534 27.9054 15.4824 27.782 15.4984C27.6588 15.5143 27.5742 15.6113 27.5932 15.7146C27.6567 16.0595 27.6925 16.4224 27.7057 16.8237Z" fill="#EEEEEE"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M27.6172 18.4687C27.6047 18.7996 27.5957 19.1305 27.5903 19.4616C27.589 19.5661 27.6652 19.6521 27.7608 19.6536C27.8564 19.6551 27.9352 19.5718 27.9365 19.4675C27.9403 19.1902 27.9471 18.9608 27.9634 18.4827L27.9683 18.3378C27.9717 18.2334 27.897 18.1458 27.8016 18.142C27.706 18.1382 27.6257 18.2195 27.6221 18.324L27.6172 18.4687ZM27.7678 22.1483C27.7905 22.2513 27.895 22.3168 28.0014 22.2951C28.1076 22.2732 28.1754 22.1721 28.1529 22.0691C28.0741 21.71 28.0187 21.3434 27.9834 20.9609C27.9736 20.8562 27.878 20.7788 27.7697 20.7882C27.6614 20.7977 27.5815 20.8903 27.5911 20.995C27.6279 21.3922 27.6856 21.7737 27.7678 22.1483ZM28.7232 24.4772C28.7792 24.563 28.9033 24.5925 29.0005 24.543C29.0976 24.4937 29.1309 24.3841 29.0751 24.2983C28.8775 23.9966 28.7013 23.6814 28.5479 23.3551C28.505 23.2634 28.3863 23.22 28.2827 23.2578C28.1792 23.2956 28.1297 23.4004 28.1728 23.4919C28.333 23.8328 28.5169 24.1621 28.7232 24.4772ZM29.633 25.9325C29.8392 26.1713 30.063 26.3944 30.3025 26.5998C30.3398 26.6315 30.3877 26.6478 30.4366 26.6455C30.4855 26.6432 30.5317 26.6224 30.5658 26.5873C30.5821 26.5704 30.5948 26.5503 30.603 26.5282C30.6113 26.5062 30.6149 26.4827 30.6136 26.4592C30.6124 26.4357 30.6063 26.4127 30.5958 26.3916C30.5853 26.3706 30.5706 26.3519 30.5525 26.3368C30.3268 26.1431 30.1158 25.9329 29.9214 25.7078C29.8848 25.6652 29.8485 25.6222 29.8125 25.579C29.7807 25.5418 29.7359 25.5182 29.6872 25.5129C29.6386 25.5075 29.5897 25.5209 29.5506 25.5502C29.5315 25.5641 29.5156 25.5818 29.5036 25.6021C29.4917 25.6224 29.4841 25.645 29.4812 25.6684C29.4784 25.6918 29.4804 25.7155 29.4871 25.7381C29.4939 25.7607 29.5052 25.7816 29.5204 25.7997C29.5574 25.8439 29.5948 25.8883 29.633 25.9325ZM32.6235 28.1392C32.7142 28.1849 32.8223 28.1424 32.8647 28.0445C32.907 27.9463 32.8677 27.8299 32.777 27.7841C32.4527 27.6215 32.1381 27.4403 31.8347 27.2414C31.7491 27.1851 31.6375 27.2142 31.5853 27.3066C31.5331 27.3988 31.5602 27.5194 31.6458 27.5757C31.9605 27.7823 32.287 27.9704 32.6235 28.1392Z" fill="#E5E5E5"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M35.2883 29.0967C35.3928 29.1234 35.4977 29.054 35.5225 28.9418C35.5472 28.8293 35.4828 28.7165 35.3783 28.6898C35.0044 28.5945 34.6348 28.4834 34.2703 28.3567C34.1683 28.3212 34.0587 28.3812 34.0254 28.4912C33.9923 28.601 34.0483 28.7189 34.1505 28.7545C34.5248 28.8846 34.9044 28.9987 35.2883 29.0965V29.0967ZM37.7828 29.48C37.8875 29.4875 37.9775 29.3959 37.9841 29.2753C37.9907 29.1545 37.9111 29.0508 37.8066 29.0432C37.4321 29.0161 37.0589 28.9735 36.6879 28.9155C36.584 28.8992 36.4885 28.9831 36.4744 29.1028C36.4604 29.2226 36.5329 29.3327 36.6369 29.349C37.0169 29.4085 37.3993 29.4522 37.783 29.48H37.7828ZM40.464 29.2169C40.568 29.2065 40.6417 29.1305 40.6286 29.0468C40.6158 28.9635 40.5211 28.9041 40.4174 28.9145C40.0551 28.9508 39.6817 28.975 39.3005 28.9867C39.1958 28.9899 39.1144 29.0608 39.1185 29.1449C39.1225 29.229 39.2105 29.2946 39.3151 29.2912C39.6988 29.2795 40.082 29.2548 40.464 29.2169ZM43.156 28.7148C43.2562 28.6732 43.3026 28.5606 43.2599 28.4631C43.2385 28.4159 43.1995 28.379 43.1513 28.3601C43.1031 28.3412 43.0494 28.3419 43.0016 28.362C42.6464 28.5079 42.2817 28.6294 41.9099 28.7254C41.8852 28.7315 41.862 28.7424 41.8416 28.7575C41.8212 28.7727 41.804 28.7918 41.7911 28.8136C41.7781 28.8355 41.7696 28.8597 41.7662 28.8849C41.7627 28.9101 41.7643 28.9357 41.7708 28.9603C41.7991 29.0627 41.9071 29.1234 42.0121 29.0958C42.4018 28.9951 42.784 28.8678 43.1562 28.7148H43.156ZM45.326 26.9135C45.3812 26.8313 45.354 26.7234 45.2656 26.6721C45.1767 26.6211 45.0601 26.6461 45.0049 26.7283C44.8241 26.9974 44.5845 27.2449 44.2899 27.4691C44.2709 27.4827 44.255 27.5003 44.2433 27.5205C44.2316 27.5407 44.2242 27.5632 44.2218 27.5864C44.2193 27.6097 44.2218 27.6332 44.229 27.6554C44.2362 27.6777 44.2481 27.6981 44.2638 27.7155C44.3299 27.7903 44.449 27.8011 44.5299 27.7397C44.8555 27.4921 45.1225 27.216 45.326 26.9135ZM46.8046 25.0747C46.8407 25.0429 46.8628 24.998 46.866 24.9499C46.8692 24.9018 46.8532 24.8544 46.8216 24.8181C46.8059 24.8001 46.7869 24.7854 46.7655 24.7748C46.7441 24.7642 46.7209 24.758 46.6971 24.7564C46.6733 24.7548 46.6494 24.7579 46.6268 24.7656C46.6043 24.7733 46.5834 24.7853 46.5655 24.8011C46.2859 25.0458 46.0227 25.3086 45.7775 25.5878C45.7458 25.6241 45.7297 25.6716 45.7329 25.7197C45.736 25.7678 45.7581 25.8127 45.7943 25.8446C45.8122 25.8604 45.833 25.8724 45.8556 25.8801C45.8781 25.8878 45.902 25.891 45.9257 25.8894C45.9495 25.8878 45.9728 25.8816 45.9941 25.871C46.0155 25.8604 46.0345 25.8458 46.0502 25.8278C46.2849 25.5606 46.5368 25.309 46.8044 25.0747H46.8046ZM49.036 23.5975C49.126 23.5489 49.1602 23.4354 49.1125 23.3439C49.1015 23.3222 49.0862 23.303 49.0676 23.2873C49.049 23.2716 49.0275 23.2598 49.0043 23.2526C48.9811 23.2453 48.9566 23.2428 48.9324 23.2451C48.9082 23.2474 48.8847 23.2545 48.8633 23.266C48.5322 23.4443 48.2091 23.6372 47.8952 23.8441C47.8099 23.9004 47.7856 24.0167 47.8409 24.1036C47.8965 24.1905 48.0106 24.2151 48.096 24.1588C48.4009 23.9579 48.7145 23.7706 49.036 23.5975ZM51.454 22.4735C51.5579 22.4415 51.6148 22.3367 51.581 22.239C51.5471 22.1411 51.4354 22.0878 51.3315 22.1195C50.9508 22.236 50.5742 22.3654 50.2024 22.5077C50.1011 22.5464 50.0523 22.6551 50.0935 22.7501C50.1347 22.8454 50.2504 22.8911 50.3517 22.8524C50.7147 22.7135 51.0823 22.5872 51.454 22.4735Z" fill="#EEEEEE"/>
-<path d="M27.7796 18.3307C28.3014 18.3307 28.7244 17.9076 28.7244 17.3858C28.7244 16.864 28.3014 16.4409 27.7796 16.4409C27.2577 16.4409 26.8347 16.864 26.8347 17.3858C26.8347 17.9076 27.2577 18.3307 27.7796 18.3307Z" fill="white" stroke="#EEEEEE"/>
-<path d="M45.3543 27.4016C45.8761 27.4016 46.2992 26.9786 46.2992 26.4567C46.2992 25.9349 45.8761 25.5118 45.3543 25.5118C44.8325 25.5118 44.4094 25.9349 44.4094 26.4567C44.4094 26.9786 44.8325 27.4016 45.3543 27.4016Z" fill="white" stroke="#EEEEEE"/>
-<path d="M4.16876 10.9607C4.16876 10.9607 0.867338 17.1969 1.90104 20.9764C2.93474 24.756 5.11364 27.0237 9.08214 29.2914C13.0506 31.5591 15.2125 28.3465 20.5984 30.4253C25.9842 32.504 26.0787 38.5513 26.0787 38.5513" stroke="#B5A7DD" stroke-width="0.4" stroke-linecap="round" stroke-dasharray="8 10"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M49.8898 47.6221C49.8898 39.5857 43.375 33.0709 35.3386 33.0709C27.3022 33.0709 20.7874 39.5857 20.7874 47.6221" fill="white"/>
-<path d="M49.8898 47.6221C49.8898 39.5857 43.375 33.0709 35.3386 33.0709C27.3022 33.0709 20.7874 39.5857 20.7874 47.6221" stroke="#EEEEEE" stroke-linecap="round"/>
-<path d="M41.1969 43.8425C42.8668 43.8425 44.2205 42.4888 44.2205 40.8189C44.2205 39.149 42.8668 37.7953 41.1969 37.7953C39.527 37.7953 38.1732 39.149 38.1732 40.8189C38.1732 42.4888 39.527 43.8425 41.1969 43.8425Z" stroke="#EEEEEE"/>
-<path d="M28.8189 40.441C29.7061 40.441 30.4252 39.7218 30.4252 38.8347C30.4252 37.9476 29.7061 37.2284 28.8189 37.2284C27.9318 37.2284 27.2126 37.9476 27.2126 38.8347C27.2126 39.7218 27.9318 40.441 28.8189 40.441Z" stroke="#EEEEEE"/>
-<path d="M24.9449 44.9764C25.4667 44.9764 25.8898 44.5534 25.8898 44.0316C25.8898 43.5097 25.4667 43.0867 24.9449 43.0867C24.423 43.0867 24 43.5097 24 44.0316C24 44.5534 24.423 44.9764 24.9449 44.9764Z" stroke="#EEEEEE"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M45.703 3.43286L46.1389 3.54966C46.346 2.7768 47.1318 2.3159 47.8939 2.5201L48.0123 2.078C47.0096 1.80933 45.9757 2.416 45.7032 3.43291L45.703 3.43286Z" fill="#FC8A51"/>
-<path d="M47.9471 2.61347C48.1478 2.66723 48.3546 2.54588 48.4091 2.34244C48.4636 2.139 48.3452 1.93051 48.1445 1.87675C47.9439 1.823 47.7371 1.94434 47.6826 2.14778C47.6281 2.35122 47.7465 2.55972 47.9471 2.61347Z" fill="#FC8A51"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M45.412 3.35511L44.9761 3.23831C45.1832 2.46545 44.7332 1.67342 43.9711 1.46921L44.0896 1.02711C45.0922 1.29577 45.6843 2.33814 45.4119 3.35506L45.412 3.35511Z" fill="#FC8A51"/>
-<path d="M43.8787 1.52354C43.6781 1.46979 43.5597 1.26129 43.6142 1.05785C43.6687 0.854411 43.8755 0.733068 44.0761 0.786823C44.2768 0.840578 44.3952 1.04908 44.3407 1.25252C44.2862 1.45596 44.0794 1.5773 43.8787 1.52354Z" fill="#FC8A51"/>
-<path d="M47.8422 7.79545L46.7544 7.50399C46.6536 7.47698 46.55 7.5368 46.523 7.63762C46.496 7.73843 46.5558 7.84205 46.6566 7.86907L47.7444 8.16052C47.8452 8.18754 47.9488 8.12771 47.9758 8.0269C48.0028 7.92608 47.943 7.82246 47.8422 7.79545Z" fill="#FC8A51"/>
-<path d="M47.4759 9.27595L46.5007 8.7129C46.4103 8.66071 46.2947 8.69168 46.2425 8.78207C46.1904 8.87245 46.2213 8.98803 46.3117 9.04021L47.287 9.60327C47.3773 9.65545 47.4929 9.62448 47.5451 9.5341C47.5973 9.44371 47.5663 9.32813 47.4759 9.27595Z" fill="#FC8A51"/>
-<path d="M48.1795 6.35876L47.0534 6.35876C46.949 6.35876 46.8644 6.44337 46.8644 6.54774C46.8644 6.65211 46.949 6.73671 47.0534 6.73671L48.1795 6.73671C48.2839 6.73671 48.3685 6.6521 48.3685 6.54774C48.3685 6.44337 48.2839 6.35876 48.1795 6.35876Z" fill="#FC8A51"/>
-<path d="M41.3783 6.06356L42.4661 6.35502C42.5669 6.38203 42.6267 6.48565 42.5997 6.58647C42.5727 6.68728 42.469 6.74711 42.3682 6.72009L41.2805 6.42863C41.1797 6.40162 41.1198 6.298 41.1469 6.19719C41.1739 6.09637 41.2775 6.03655 41.3783 6.06356Z" fill="#FC8A51"/>
-<path d="M40.9549 7.52856L42.081 7.52856C42.1854 7.52856 42.27 7.61317 42.27 7.71754C42.27 7.82191 42.1854 7.90651 42.081 7.90651L40.9549 7.90651C40.8506 7.90651 40.7659 7.8219 40.7659 7.71754C40.7659 7.61317 40.8505 7.52856 40.9549 7.52856Z" fill="#FC8A51"/>
-<path d="M41.8041 4.65032L42.7794 5.21337C42.8698 5.26556 42.9007 5.38113 42.8485 5.47152C42.7964 5.5619 42.6808 5.59287 42.5904 5.54069L41.6151 4.97763C41.5248 4.92545 41.4938 4.80987 41.546 4.71949C41.5982 4.6291 41.7137 4.59813 41.8041 4.65032Z" fill="#FC8A51"/>
-<path d="M47.3515 6.28652C47.7063 4.96254 46.9206 3.60168 45.5967 3.24693C44.2728 2.89219 42.912 3.6779 42.5572 5.00187L42.1051 6.68907C41.7504 8.01304 42.536 9.3739 43.86 9.72865C45.1839 10.0834 46.5447 9.29768 46.8995 7.97371L47.3515 6.28652Z" stroke="#FC8A51"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M42.7135 5.00037L47.0721 6.16826L46.9254 6.71587L42.5668 5.54798L42.7135 5.00037Z" fill="#FC8A51"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M17.4662 2.50359L17.1869 2.57842C17.1557 2.46241 17.1019 2.35369 17.0286 2.25847C16.9554 2.16325 16.8641 2.0834 16.7599 2.02348C16.6558 1.96355 16.5409 1.92474 16.4217 1.90924C16.3026 1.89374 16.1816 1.90187 16.0656 1.93316L15.9909 1.6546C16.634 1.48229 17.2944 1.86252 17.4662 2.50359Z" fill="#EEEEEE"/>
-<path d="M16.0312 1.99224C15.9025 2.02671 15.7704 1.95069 15.736 1.82246C15.7017 1.69423 15.7781 1.56233 15.9067 1.52786C16.0354 1.4934 16.1675 1.56941 16.2019 1.69764C16.2362 1.82587 16.1598 1.95777 16.0312 1.99224Z" fill="#EEEEEE"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M17.6526 2.45366L17.9318 2.37883C17.9009 2.26274 17.8931 2.14169 17.909 2.02259C17.9248 1.9035 17.964 1.78869 18.0242 1.68473C18.0844 1.58076 18.1645 1.48969 18.2599 1.4167C18.3554 1.3437 18.4642 1.29023 18.5804 1.25932L18.5057 0.980773C17.8626 1.15308 17.4808 1.81259 17.6526 2.45366Z" fill="#EEEEEE"/>
-<path d="M18.6393 1.29344C18.7679 1.25898 18.8444 1.12708 18.81 0.998847C18.7756 0.870613 18.6435 0.794601 18.5149 0.82907C18.3862 0.863538 18.3098 0.995433 18.3442 1.12367C18.3785 1.2519 18.5107 1.32791 18.6393 1.29344Z" fill="#EEEEEE"/>
-<path d="M16.1505 5.24482L16.7167 5.0931C16.8175 5.06609 16.9211 5.12591 16.9481 5.22673C16.9752 5.32754 16.9153 5.43116 16.8145 5.45817L16.2483 5.60989C16.1475 5.63691 16.0439 5.57708 16.0168 5.47627C15.9898 5.37546 16.0497 5.27183 16.1505 5.24482Z" fill="#EEEEEE"/>
-<path d="M16.3578 6.1703L16.8655 5.8772C16.9559 5.82502 17.0714 5.85599 17.1236 5.94637C17.1758 6.03676 17.1448 6.15233 17.0545 6.20452L16.5468 6.49762C16.4564 6.5498 16.3408 6.51883 16.2886 6.42845C16.2365 6.33806 16.2674 6.22249 16.3578 6.1703Z" fill="#EEEEEE"/>
-<path d="M15.9573 4.35278L16.5435 4.35278C16.6479 4.35278 16.7325 4.43739 16.7325 4.54176C16.7325 4.64613 16.6479 4.73074 16.5435 4.73074L15.9573 4.73074C15.853 4.73074 15.7684 4.64613 15.7684 4.54176C15.7684 4.43739 15.853 4.35278 15.9573 4.35278Z" fill="#EEEEEE"/>
-<path d="M20.1626 4.16985L19.5964 4.32157C19.4956 4.34859 19.4357 4.45221 19.4628 4.55302C19.4898 4.65383 19.5934 4.71366 19.6942 4.68665L20.2604 4.53493C20.3612 4.50791 20.4211 4.40429 20.3941 4.30348C20.367 4.20267 20.2634 4.14284 20.1626 4.16985Z" fill="#EEEEEE"/>
-<path d="M20.4451 5.07507L19.8589 5.07507C19.7545 5.07507 19.6699 5.15968 19.6699 5.26405C19.6699 5.36842 19.7545 5.45303 19.8589 5.45303L20.4451 5.45303C20.5494 5.45303 20.634 5.36842 20.634 5.26405C20.634 5.15968 20.5494 5.07507 20.4451 5.07507Z" fill="#EEEEEE"/>
-<path d="M19.8834 3.30076L19.3757 3.59387C19.2853 3.64605 19.2543 3.76163 19.3065 3.85201C19.3587 3.9424 19.4743 3.97337 19.5647 3.92118L20.0723 3.62808C20.1627 3.5759 20.1937 3.46032 20.1415 3.36993C20.0893 3.27955 19.9737 3.24858 19.8834 3.30076Z" fill="#EEEEEE"/>
-<path d="M17.5341 2.38566L17.5343 2.38561C18.3829 2.15821 19.2552 2.66184 19.4826 3.51048L19.7541 4.52356C19.9815 5.3722 19.4778 6.24449 18.6292 6.47189L18.629 6.47193C17.7804 6.69933 16.9081 6.19571 16.6807 5.34707L16.4092 4.33398C16.1819 3.48534 16.6855 2.61305 17.5341 2.38566Z" stroke="#EEEEEE"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3759 3.48474L16.582 4.23337L16.6798 4.59844L19.4737 3.84982L19.3759 3.48474Z" fill="#EEEEEE"/>
-<path d="M1.98426 23.0551C2.87139 23.0551 3.59056 22.336 3.59056 21.4488C3.59056 20.5617 2.87139 19.8425 1.98426 19.8425C1.09712 19.8425 0.37796 20.5617 0.37796 21.4488C0.37796 22.336 1.09712 23.0551 1.98426 23.0551Z" fill="white" stroke="#B5A7DD" stroke-width="0.4"/>
-<path d="M32.7874 24.9449C33.4658 24.9449 34.0158 24.3949 34.0158 23.7165C34.0158 23.0381 33.4658 22.4882 32.7874 22.4882C32.109 22.4882 31.5591 23.0381 31.5591 23.7165C31.5591 24.3949 32.109 24.9449 32.7874 24.9449Z" fill="white"/>
-<path d="M10.1102 31.9371C11.4148 31.9371 12.4724 30.8795 12.4724 29.5749C12.4724 28.2702 11.4148 27.2126 10.1102 27.2126C8.80561 27.2126 7.74802 28.2702 7.74802 29.5749C7.74802 30.8795 8.80561 31.9371 10.1102 31.9371Z" fill="white" stroke="#6B4FBB"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9.77635 29.759L9.44262 29.4251C9.38944 29.3719 9.31732 29.3421 9.24211 29.3421C9.16691 29.3421 9.09479 29.3719 9.04161 29.4251C8.98843 29.4783 8.95856 29.5504 8.95856 29.6256C8.95856 29.7008 8.98843 29.773 9.04161 29.8261L9.57566 30.3602H9.57585V30.3604C9.68734 30.4719 9.86592 30.4713 9.97629 30.3609L11.1801 29.1572C11.2328 29.1038 11.2623 29.0319 11.2622 28.9569C11.2621 28.8819 11.2324 28.8101 11.1795 28.7569C11.1533 28.7305 11.1222 28.7095 11.0879 28.6952C11.0535 28.6808 11.0167 28.6734 10.9795 28.6733C10.9423 28.6733 10.9055 28.6805 10.8711 28.6948C10.8368 28.709 10.8055 28.7298 10.7792 28.7561L9.77635 29.759Z" fill="#FC8A51"/>
-<path d="M32.7874 24.7559C33.4658 24.7559 34.0158 24.2059 34.0158 23.5275C34.0158 22.8491 33.4658 22.2992 32.7874 22.2992C32.109 22.2992 31.5591 22.8491 31.5591 23.5275C31.5591 24.2059 32.109 24.7559 32.7874 24.7559Z" fill="white" stroke="#FC8A51"/>
-<path d="M4.53541 11.3386C5.16162 11.3386 5.66926 10.831 5.66926 10.2048C5.66926 9.57857 5.16162 9.07092 4.53541 9.07092C3.9092 9.07092 3.40155 9.57857 3.40155 10.2048C3.40155 10.831 3.9092 11.3386 4.53541 11.3386Z" fill="white" stroke="#B5A7DD" stroke-width="0.4"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M35.9339 27.1363C37.5787 25.6725 38.0151 23.182 36.8527 21.1688C35.5261 18.8708 32.6187 18.0654 30.3591 19.3701C28.0993 20.6748 27.343 23.5953 28.6698 25.8932C29.8483 27.9342 32.2732 28.7978 34.3841 28.0513L36.4437 31.619C36.563 31.8259 36.7596 31.9769 36.9903 32.0389C37.2209 32.1008 37.4667 32.0686 37.6736 31.9493C37.8802 31.8297 38.0309 31.6329 38.0926 31.4023C38.1542 31.1716 38.1218 30.9259 38.0024 30.7191L35.9339 27.1363ZM34.34 26.2653C35.8248 25.4081 36.3218 23.4889 35.4499 21.9788C34.5782 20.4686 32.6676 19.9395 31.1828 20.7967C29.6978 21.6541 29.2008 23.5733 30.0726 25.0834C30.9445 26.5934 32.8551 27.1227 34.34 26.2653Z" fill="white" stroke="#B5A7DD" stroke-width="0.5"/>
-</g>
-<defs>
-<clipPath id="clip0">
-<rect width="80.315" height="48" fill="white"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/app/assets/images/learn_gitlab/merge_request_created.svg b/app/assets/images/learn_gitlab/merge_request_created.svg
deleted file mode 100644
index b8137a60f06..00000000000
--- a/app/assets/images/learn_gitlab/merge_request_created.svg
+++ /dev/null
@@ -1,107 +0,0 @@
-<svg width="79" height="47" viewBox="0 0 79 47" fill="none" xmlns="http://www.w3.org/2000/svg">
-<g clip-path="url(#clip0)">
-<path d="M27.0655 1.96289H5.80354C4.48549 1.96289 3.41699 3.03139 3.41699 4.34944C3.41699 5.6675 4.48549 6.73599 5.80354 6.73599H27.0655C28.3836 6.73599 29.4521 5.6675 29.4521 4.34944C29.4521 3.03139 28.3836 1.96289 27.0655 1.96289Z" fill="#F9F9F9"/>
-<path d="M23.1603 11.5092H-0.705247C-2.0233 11.5092 -3.0918 12.5776 -3.0918 13.8957C-3.0918 15.2138 -2.0233 16.2823 -0.705247 16.2823H23.1603C24.4783 16.2823 25.5468 15.2138 25.5468 13.8957C25.5468 12.5776 24.4783 11.5092 23.1603 11.5092Z" fill="#F9F9F9"/>
-<path d="M80.8713 16.2822H44.4222C43.1041 16.2822 42.0356 17.3507 42.0356 18.6688C42.0356 19.9868 43.1041 21.0553 44.4222 21.0553H80.8713C82.1894 21.0553 83.2579 19.9868 83.2579 18.6688C83.2579 17.3507 82.1894 16.2822 80.8713 16.2822Z" fill="#F9F9F9"/>
-<path d="M56.789 44.7039H27.2825C25.9645 44.7039 24.896 45.7724 24.896 47.0904C24.896 48.4085 25.9645 49.477 27.2825 49.477H56.789C58.107 49.477 59.1755 48.4085 59.1755 47.0904C59.1755 45.7724 58.107 44.7039 56.789 44.7039Z" fill="#F9F9F9"/>
-<path d="M43.1205 35.3746H13.6141C12.296 35.3746 11.2275 36.4431 11.2275 37.7612C11.2275 39.0792 12.296 40.1477 13.6141 40.1477H43.1205C44.4386 40.1477 45.5071 39.0792 45.5071 37.7612C45.5071 36.4431 44.4386 35.3746 43.1205 35.3746Z" fill="#F9F9F9"/>
-<path d="M77.1829 25.8284H6.02034C4.70228 25.8284 3.63379 26.8969 3.63379 28.2149C3.63379 29.533 4.70228 30.6015 6.02034 30.6015H77.1829C78.501 30.6015 79.5695 29.533 79.5695 28.2149C79.5695 26.8969 78.501 25.8284 77.1829 25.8284Z" fill="#F9F9F9"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M30.103 6.73596H1.46436H6.12898C7.43073 6.73596 8.51553 7.82076 8.51553 9.12251C8.51553 10.4243 7.43073 11.5091 6.12898 11.5091H1.46436H30.103H22.1839C20.8822 11.5091 19.7974 10.4243 19.7974 9.12251C19.7974 7.82076 20.8822 6.73596 22.1839 6.73596H30.103ZM84.7766 21.0553H59.3924H67.3114C68.6132 21.0553 69.698 22.1401 69.698 23.4418C69.698 24.7436 68.6132 25.8284 67.3114 25.8284H59.3924H84.7766H76.8576C75.5559 25.8284 74.4711 24.7436 74.4711 23.4418C74.4711 22.1401 75.5559 21.0553 76.8576 21.0553H84.7766ZM31.8386 30.6015H6.45441H14.3734C15.6752 30.6015 16.76 31.6862 16.76 32.988C16.76 34.2898 15.6752 35.3746 14.3734 35.3746H6.45441H31.8386H23.9196C22.6179 35.3746 21.5331 34.2898 21.5331 32.988C21.5331 31.6862 22.6179 30.6015 23.9196 30.6015H31.8386ZM48.1106 40.1477H22.7263H27.391C28.6927 40.1477 29.7775 41.2324 29.7775 42.5342C29.7775 43.836 28.6927 44.9207 27.391 44.9207H22.7263H48.1106H36.9372C35.6354 44.9207 34.5506 43.836 34.5506 42.5342C34.5506 41.2324 35.6354 40.1477 36.9372 40.1477H48.1106Z" fill="#F9F9F9"/>
-<path d="M68.0708 4.78333H12.0954C10.8971 4.78333 9.92578 5.75468 9.92578 6.95292V41.4494C9.92578 42.6476 10.8971 43.619 12.0954 43.619H68.0708C69.269 43.619 70.2404 42.6476 70.2404 41.4494V6.95292C70.2404 5.75468 69.269 4.78333 68.0708 4.78333Z" fill="white" stroke="#EEEEEE" stroke-width="2"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7935 11.0751H69.5894V11.9429H10.7935V11.0751Z" fill="#EEEEEE"/>
-<path d="M18.387 18.0178H17.5192C17.3994 18.0178 17.3022 18.115 17.3022 18.2348C17.3022 18.3546 17.3994 18.4517 17.5192 18.4517H18.387C18.5069 18.4517 18.604 18.3546 18.604 18.2348C18.604 18.115 18.5069 18.0178 18.387 18.0178Z" fill="#B5A7DD"/>
-<path d="M23.3771 17.8009H20.9906C20.7509 17.8009 20.5566 17.9952 20.5566 18.2348C20.5566 18.4745 20.7509 18.6687 20.9906 18.6687H23.3771C23.6167 18.6687 23.811 18.4745 23.811 18.2348C23.811 17.9952 23.6167 17.8009 23.3771 17.8009Z" fill="#EEEEEE"/>
-<path d="M35.7438 17.8009H33.3573C33.1176 17.8009 32.9233 17.9952 32.9233 18.2348C32.9233 18.4745 33.1176 18.6687 33.3573 18.6687H35.7438C35.9835 18.6687 36.1777 18.4745 36.1777 18.2348C36.1777 17.9952 35.9835 17.8009 35.7438 17.8009Z" fill="#EEEEEE"/>
-<path d="M28.5841 22.574H26.1976C25.9579 22.574 25.7637 22.7682 25.7637 23.0079C25.7637 23.2475 25.9579 23.4418 26.1976 23.4418H28.5841C28.8238 23.4418 29.0181 23.2475 29.0181 23.0079C29.0181 22.7682 28.8238 22.574 28.5841 22.574Z" fill="#EEEEEE"/>
-<path d="M31.6217 20.1875H29.2352C28.9955 20.1875 28.8013 20.3818 28.8013 20.6214C28.8013 20.8611 28.9955 21.0553 29.2352 21.0553H31.6217C31.8614 21.0553 32.0557 20.8611 32.0557 20.6214C32.0557 20.3818 31.8614 20.1875 31.6217 20.1875Z" fill="#FC6D26"/>
-<path opacity="0.5" d="M31.6216 17.8009H28.1502C27.9106 17.8009 27.7163 17.9952 27.7163 18.2348C27.7163 18.4745 27.9106 18.6687 28.1502 18.6687H31.6216C31.8612 18.6687 32.0555 18.4745 32.0555 18.2348C32.0555 17.9952 31.8612 17.8009 31.6216 17.8009Z" fill="#FC6D26"/>
-<path d="M24.4619 22.574H20.9906C20.7509 22.574 20.5566 22.7682 20.5566 23.0079C20.5566 23.2475 20.7509 23.4418 20.9906 23.4418H24.4619C24.7016 23.4418 24.8958 23.2475 24.8958 23.0079C24.8958 22.7682 24.7016 22.574 24.4619 22.574Z" fill="#EEEEEE"/>
-<path d="M27.4995 20.1875H24.0282C23.7885 20.1875 23.5942 20.3818 23.5942 20.6214C23.5942 20.8611 23.7885 21.0553 24.0282 21.0553H27.4995C27.7392 21.0553 27.9334 20.8611 27.9334 20.6214C27.9334 20.3818 27.7392 20.1875 27.4995 20.1875Z" fill="#EEEEEE"/>
-<path d="M26.4144 17.8009H25.1126C24.873 17.8009 24.6787 17.9952 24.6787 18.2348C24.6787 18.4745 24.873 18.6687 25.1126 18.6687H26.4144C26.654 18.6687 26.8483 18.4745 26.8483 18.2348C26.8483 17.9952 26.654 17.8009 26.4144 17.8009Z" fill="#FC6D26"/>
-<path d="M22.2923 20.1875H20.9906C20.7509 20.1875 20.5566 20.3818 20.5566 20.6214C20.5566 20.8611 20.7509 21.0553 20.9906 21.0553H22.2923C22.532 21.0553 22.7262 20.8611 22.7262 20.6214C22.7262 20.3818 22.532 20.1875 22.2923 20.1875Z" fill="#EEEEEE"/>
-<path d="M18.387 20.4044H17.5192C17.3994 20.4044 17.3022 20.5016 17.3022 20.6214C17.3022 20.7412 17.3994 20.8383 17.5192 20.8383H18.387C18.5069 20.8383 18.604 20.7412 18.604 20.6214C18.604 20.5016 18.5069 20.4044 18.387 20.4044Z" fill="#B5A7DD"/>
-<path d="M18.387 22.791H17.5192C17.3994 22.791 17.3022 22.8882 17.3022 23.008C17.3022 23.1278 17.3994 23.2249 17.5192 23.2249H18.387C18.5069 23.2249 18.604 23.1278 18.604 23.008C18.604 22.8882 18.5069 22.791 18.387 22.791Z" fill="#B5A7DD"/>
-<path d="M18.387 25.1774H17.5192C17.3994 25.1774 17.3022 25.2745 17.3022 25.3943C17.3022 25.5142 17.3994 25.6113 17.5192 25.6113H18.387C18.5069 25.6113 18.604 25.5142 18.604 25.3943C18.604 25.2745 18.5069 25.1774 18.387 25.1774Z" fill="#B5A7DD"/>
-<path d="M23.3771 24.9604H20.9906C20.7509 24.9604 20.5566 25.1547 20.5566 25.3944C20.5566 25.634 20.7509 25.8283 20.9906 25.8283H23.3771C23.6167 25.8283 23.811 25.634 23.811 25.3944C23.811 25.1547 23.6167 24.9604 23.3771 24.9604Z" fill="#FC6D26"/>
-<path d="M35.7438 24.9604H33.3573C33.1176 24.9604 32.9233 25.1547 32.9233 25.3944C32.9233 25.634 33.1176 25.8283 33.3573 25.8283H35.7438C35.9835 25.8283 36.1777 25.634 36.1777 25.3944C36.1777 25.1547 35.9835 24.9604 35.7438 24.9604Z" fill="#EEEEEE"/>
-<path opacity="0.5" d="M28.5841 29.7335H26.1976C25.9579 29.7335 25.7637 29.9278 25.7637 30.1674C25.7637 30.4071 25.9579 30.6014 26.1976 30.6014H28.5841C28.8238 30.6014 29.0181 30.4071 29.0181 30.1674C29.0181 29.9278 28.8238 29.7335 28.5841 29.7335Z" fill="#FC6D26"/>
-<path d="M31.6217 27.347H29.2352C28.9955 27.347 28.8013 27.5413 28.8013 27.781C28.8013 28.0206 28.9955 28.2149 29.2352 28.2149H31.6217C31.8614 28.2149 32.0557 28.0206 32.0557 27.781C32.0557 27.5413 31.8614 27.347 31.6217 27.347Z" fill="#EEEEEE"/>
-<path d="M31.6216 24.9604H28.1502C27.9106 24.9604 27.7163 25.1547 27.7163 25.3944C27.7163 25.634 27.9106 25.8283 28.1502 25.8283H31.6216C31.8612 25.8283 32.0555 25.634 32.0555 25.3944C32.0555 25.1547 31.8612 24.9604 31.6216 24.9604Z" fill="#FC6D26"/>
-<path d="M24.4619 29.7335H20.9906C20.7509 29.7335 20.5566 29.9278 20.5566 30.1674C20.5566 30.4071 20.7509 30.6014 20.9906 30.6014H24.4619C24.7016 30.6014 24.8958 30.4071 24.8958 30.1674C24.8958 29.9278 24.7016 29.7335 24.4619 29.7335Z" fill="#FC6D26"/>
-<path d="M27.4995 27.347H24.0282C23.7885 27.347 23.5942 27.5413 23.5942 27.781C23.5942 28.0206 23.7885 28.2149 24.0282 28.2149H27.4995C27.7392 28.2149 27.9334 28.0206 27.9334 27.781C27.9334 27.5413 27.7392 27.347 27.4995 27.347Z" fill="#EEEEEE"/>
-<path opacity="0.5" d="M26.4144 24.9604H25.1126C24.873 24.9604 24.6787 25.1547 24.6787 25.3944C24.6787 25.634 24.873 25.8283 25.1126 25.8283H26.4144C26.654 25.8283 26.8483 25.634 26.8483 25.3944C26.8483 25.1547 26.654 24.9604 26.4144 24.9604Z" fill="#FC6D26"/>
-<path d="M22.2923 27.347H20.9906C20.7509 27.347 20.5566 27.5413 20.5566 27.781C20.5566 28.0206 20.7509 28.2149 20.9906 28.2149H22.2923C22.532 28.2149 22.7262 28.0206 22.7262 27.781C22.7262 27.5413 22.532 27.347 22.2923 27.347Z" fill="#EEEEEE"/>
-<path d="M18.387 27.564H17.5192C17.3994 27.564 17.3022 27.6611 17.3022 27.7809C17.3022 27.9007 17.3994 27.9979 17.5192 27.9979H18.387C18.5069 27.9979 18.604 27.9007 18.604 27.7809C18.604 27.6611 18.5069 27.564 18.387 27.564Z" fill="#B5A7DD"/>
-<path d="M18.387 29.9506H17.5192C17.3994 29.9506 17.3022 30.0477 17.3022 30.1675C17.3022 30.2873 17.3994 30.3845 17.5192 30.3845H18.387C18.5069 30.3845 18.604 30.2873 18.604 30.1675C18.604 30.0477 18.5069 29.9506 18.387 29.9506Z" fill="#B5A7DD"/>
-<path d="M18.387 32.337H17.5192C17.3994 32.337 17.3022 32.4342 17.3022 32.554C17.3022 32.6738 17.3994 32.771 17.5192 32.771H18.387C18.5069 32.771 18.604 32.6738 18.604 32.554C18.604 32.4342 18.5069 32.337 18.387 32.337Z" fill="#B5A7DD"/>
-<path d="M23.3771 32.1201H20.9906C20.7509 32.1201 20.5566 32.3144 20.5566 32.554C20.5566 32.7937 20.7509 32.988 20.9906 32.988H23.3771C23.6167 32.988 23.811 32.7937 23.811 32.554C23.811 32.3144 23.6167 32.1201 23.3771 32.1201Z" fill="#EEEEEE"/>
-<path d="M35.7438 32.1201H33.3573C33.1176 32.1201 32.9233 32.3144 32.9233 32.554C32.9233 32.7937 33.1176 32.988 33.3573 32.988H35.7438C35.9835 32.988 36.1777 32.7937 36.1777 32.554C36.1777 32.3144 35.9835 32.1201 35.7438 32.1201Z" fill="#EEEEEE"/>
-<path d="M28.5841 36.8932H26.1976C25.9579 36.8932 25.7637 37.0875 25.7637 37.3271C25.7637 37.5668 25.9579 37.761 26.1976 37.761H28.5841C28.8238 37.761 29.0181 37.5668 29.0181 37.3271C29.0181 37.0875 28.8238 36.8932 28.5841 36.8932Z" fill="#EEEEEE"/>
-<path d="M31.6217 34.5067H29.2352C28.9955 34.5067 28.8013 34.701 28.8013 34.9406C28.8013 35.1803 28.9955 35.3745 29.2352 35.3745H31.6217C31.8614 35.3745 32.0557 35.1803 32.0557 34.9406C32.0557 34.701 31.8614 34.5067 31.6217 34.5067Z" fill="#EEEEEE"/>
-<path d="M31.6216 32.1201H28.1502C27.9106 32.1201 27.7163 32.3144 27.7163 32.554C27.7163 32.7937 27.9106 32.988 28.1502 32.988H31.6216C31.8612 32.988 32.0555 32.7937 32.0555 32.554C32.0555 32.3144 31.8612 32.1201 31.6216 32.1201Z" fill="#FC6D26"/>
-<path d="M24.4619 36.8932H20.9906C20.7509 36.8932 20.5566 37.0875 20.5566 37.3271C20.5566 37.5668 20.7509 37.761 20.9906 37.761H24.4619C24.7016 37.761 24.8958 37.5668 24.8958 37.3271C24.8958 37.0875 24.7016 36.8932 24.4619 36.8932Z" fill="#EEEEEE"/>
-<path d="M27.4995 34.5067H24.0282C23.7885 34.5067 23.5942 34.701 23.5942 34.9406C23.5942 35.1803 23.7885 35.3745 24.0282 35.3745H27.4995C27.7392 35.3745 27.9334 35.1803 27.9334 34.9406C27.9334 34.701 27.7392 34.5067 27.4995 34.5067Z" fill="#EEEEEE"/>
-<path opacity="0.5" d="M26.4144 32.1201H25.1126C24.873 32.1201 24.6787 32.3144 24.6787 32.554C24.6787 32.7937 24.873 32.988 25.1126 32.988H26.4144C26.654 32.988 26.8483 32.7937 26.8483 32.554C26.8483 32.3144 26.654 32.1201 26.4144 32.1201Z" fill="#FC6D26"/>
-<path d="M22.2923 34.5067H20.9906C20.7509 34.5067 20.5566 34.701 20.5566 34.9406C20.5566 35.1803 20.7509 35.3745 20.9906 35.3745H22.2923C22.532 35.3745 22.7262 35.1803 22.7262 34.9406C22.7262 34.701 22.532 34.5067 22.2923 34.5067Z" fill="#EEEEEE"/>
-<path d="M18.387 34.7236H17.5192C17.3994 34.7236 17.3022 34.8208 17.3022 34.9406C17.3022 35.0604 17.3994 35.1575 17.5192 35.1575H18.387C18.5069 35.1575 18.604 35.0604 18.604 34.9406C18.604 34.8208 18.5069 34.7236 18.387 34.7236Z" fill="#B5A7DD"/>
-<path d="M18.387 37.1102H17.5192C17.3994 37.1102 17.3022 37.2074 17.3022 37.3272C17.3022 37.447 17.3994 37.5442 17.5192 37.5442H18.387C18.5069 37.5442 18.604 37.447 18.604 37.3272C18.604 37.2074 18.5069 37.1102 18.387 37.1102Z" fill="#B5A7DD"/>
-<path d="M45.073 17.8008H44.2052C44.0854 17.8008 43.9883 17.8979 43.9883 18.0177C43.9883 18.1376 44.0854 18.2347 44.2052 18.2347H45.073C45.1929 18.2347 45.29 18.1376 45.29 18.0177C45.29 17.8979 45.1929 17.8008 45.073 17.8008Z" fill="#FDE5D8"/>
-<path d="M50.0631 17.5839H47.6766C47.4369 17.5839 47.2427 17.7781 47.2427 18.0178C47.2427 18.2574 47.4369 18.4517 47.6766 18.4517H50.0631C50.3028 18.4517 50.4971 18.2574 50.4971 18.0178C50.4971 17.7781 50.3028 17.5839 50.0631 17.5839Z" fill="#EEEEEE"/>
-<path d="M62.4299 17.5839H60.0433C59.8036 17.5839 59.6094 17.7781 59.6094 18.0178C59.6094 18.2574 59.8036 18.4517 60.0433 18.4517H62.4299C62.6695 18.4517 62.8638 18.2574 62.8638 18.0178C62.8638 17.7781 62.6695 17.5839 62.4299 17.5839Z" fill="#EEEEEE"/>
-<path opacity="0.5" d="M55.2702 22.3569H52.8836C52.644 22.3569 52.4497 22.5512 52.4497 22.7909C52.4497 23.0305 52.644 23.2248 52.8836 23.2248H55.2702C55.5098 23.2248 55.7041 23.0305 55.7041 22.7909C55.7041 22.5512 55.5098 22.3569 55.2702 22.3569Z" fill="#6B4FBB"/>
-<path d="M58.3078 19.9705H55.9212C55.6816 19.9705 55.4873 20.1647 55.4873 20.4044C55.4873 20.644 55.6816 20.8383 55.9212 20.8383H58.3078C58.5474 20.8383 58.7417 20.644 58.7417 20.4044C58.7417 20.1647 58.5474 19.9705 58.3078 19.9705Z" fill="#6B4FBB"/>
-<path opacity="0.5" d="M58.3076 17.5839H54.8363C54.5966 17.5839 54.4023 17.7781 54.4023 18.0178C54.4023 18.2574 54.5966 18.4517 54.8363 18.4517H58.3076C58.5473 18.4517 58.7415 18.2574 58.7415 18.0178C58.7415 17.7781 58.5473 17.5839 58.3076 17.5839Z" fill="#6B4FBB"/>
-<path d="M51.1479 22.3569H47.6766C47.4369 22.3569 47.2427 22.5512 47.2427 22.7909C47.2427 23.0305 47.4369 23.2248 47.6766 23.2248H51.1479C51.3876 23.2248 51.5819 23.0305 51.5819 22.7909C51.5819 22.5512 51.3876 22.3569 51.1479 22.3569Z" fill="#6B4FBB"/>
-<path d="M54.1855 19.9705H50.7142C50.4745 19.9705 50.2803 20.1647 50.2803 20.4044C50.2803 20.644 50.4745 20.8383 50.7142 20.8383H54.1855C54.4252 20.8383 54.6195 20.644 54.6195 20.4044C54.6195 20.1647 54.4252 19.9705 54.1855 19.9705Z" fill="#EEEEEE"/>
-<path d="M53.1004 17.5839H51.7987C51.559 17.5839 51.3647 17.7781 51.3647 18.0178C51.3647 18.2574 51.559 18.4517 51.7987 18.4517H53.1004C53.3401 18.4517 53.5343 18.2574 53.5343 18.0178C53.5343 17.7781 53.3401 17.5839 53.1004 17.5839Z" fill="#6B4FBB"/>
-<path d="M48.9783 19.9705H47.6766C47.4369 19.9705 47.2427 20.1647 47.2427 20.4044C47.2427 20.644 47.4369 20.8383 47.6766 20.8383H48.9783C49.218 20.8383 49.4123 20.644 49.4123 20.4044C49.4123 20.1647 49.218 19.9705 48.9783 19.9705Z" fill="#EEEEEE"/>
-<path d="M45.073 20.1874H44.2052C44.0854 20.1874 43.9883 20.2845 43.9883 20.4043C43.9883 20.5242 44.0854 20.6213 44.2052 20.6213H45.073C45.1929 20.6213 45.29 20.5242 45.29 20.4043C45.29 20.2845 45.1929 20.1874 45.073 20.1874Z" fill="#FDE5D8"/>
-<path d="M45.073 22.574H44.2052C44.0854 22.574 43.9883 22.6711 43.9883 22.7909C43.9883 22.9108 44.0854 23.0079 44.2052 23.0079H45.073C45.1929 23.0079 45.29 22.9108 45.29 22.7909C45.29 22.6711 45.1929 22.574 45.073 22.574Z" fill="#FDE5D8"/>
-<path d="M45.073 24.9604H44.2052C44.0854 24.9604 43.9883 25.0576 43.9883 25.1774C43.9883 25.2972 44.0854 25.3944 44.2052 25.3944H45.073C45.1929 25.3944 45.29 25.2972 45.29 25.1774C45.29 25.0576 45.1929 24.9604 45.073 24.9604Z" fill="#FDE5D8"/>
-<path d="M50.0631 24.7435H47.6766C47.4369 24.7435 47.2427 24.9378 47.2427 25.1774C47.2427 25.4171 47.4369 25.6114 47.6766 25.6114H50.0631C50.3028 25.6114 50.4971 25.4171 50.4971 25.1774C50.4971 24.9378 50.3028 24.7435 50.0631 24.7435Z" fill="#EEEEEE"/>
-<path d="M59.3922 22.3569H57.0057C56.766 22.3569 56.5718 22.5512 56.5718 22.7909C56.5718 23.0305 56.766 23.2248 57.0057 23.2248H59.3922C59.6319 23.2248 59.8262 23.0305 59.8262 22.7909C59.8262 22.5512 59.6319 22.3569 59.3922 22.3569Z" fill="#EEEEEE"/>
-<path opacity="0.5" d="M55.2702 29.5166H52.8836C52.644 29.5166 52.4497 29.7109 52.4497 29.9505C52.4497 30.1902 52.644 30.3844 52.8836 30.3844H55.2702C55.5098 30.3844 55.7041 30.1902 55.7041 29.9505C55.7041 29.7109 55.5098 29.5166 55.2702 29.5166Z" fill="#6B4FBB"/>
-<path d="M53.1007 27.1301H50.7142C50.4745 27.1301 50.2803 27.3244 50.2803 27.564C50.2803 27.8037 50.4745 27.998 50.7142 27.998H53.1007C53.3404 27.998 53.5347 27.8037 53.5347 27.564C53.5347 27.3244 53.3404 27.1301 53.1007 27.1301Z" fill="#6B4FBB"/>
-<path d="M58.3076 24.7435H54.8363C54.5966 24.7435 54.4023 24.9378 54.4023 25.1774C54.4023 25.4171 54.5966 25.6114 54.8363 25.6114H58.3076C58.5473 25.6114 58.7415 25.4171 58.7415 25.1774C58.7415 24.9378 58.5473 24.7435 58.3076 24.7435Z" fill="#6B4FBB"/>
-<path d="M51.1479 29.5166H47.6766C47.4369 29.5166 47.2427 29.7109 47.2427 29.9505C47.2427 30.1902 47.4369 30.3844 47.6766 30.3844H51.1479C51.3876 30.3844 51.5819 30.1902 51.5819 29.9505C51.5819 29.7109 51.3876 29.5166 51.1479 29.5166Z" fill="#EEEEEE"/>
-<path d="M53.1004 24.7435H51.7987C51.559 24.7435 51.3647 24.9378 51.3647 25.1774C51.3647 25.4171 51.559 25.6114 51.7987 25.6114H53.1004C53.3401 25.6114 53.5343 25.4171 53.5343 25.1774C53.5343 24.9378 53.3401 24.7435 53.1004 24.7435Z" fill="#EEEEEE"/>
-<path d="M48.9783 27.1301H47.6766C47.4369 27.1301 47.2427 27.3244 47.2427 27.564C47.2427 27.8037 47.4369 27.998 47.6766 27.998H48.9783C49.218 27.998 49.4123 27.8037 49.4123 27.564C49.4123 27.3244 49.218 27.1301 48.9783 27.1301Z" fill="#EEEEEE"/>
-<path d="M56.138 27.1301H54.8363C54.5966 27.1301 54.4023 27.3244 54.4023 27.564C54.4023 27.8037 54.5966 27.998 54.8363 27.998H56.138C56.3777 27.998 56.5719 27.8037 56.5719 27.564C56.5719 27.3244 56.3777 27.1301 56.138 27.1301Z" fill="#EEEEEE"/>
-<path d="M59.1756 27.1301H57.8739C57.6342 27.1301 57.4399 27.3244 57.4399 27.564C57.4399 27.8037 57.6342 27.998 57.8739 27.998H59.1756C59.4153 27.998 59.6095 27.8037 59.6095 27.564C59.6095 27.3244 59.4153 27.1301 59.1756 27.1301Z" fill="#EEEEEE"/>
-<path d="M62.43 22.3569H61.1283C60.8886 22.3569 60.6943 22.5512 60.6943 22.7909C60.6943 23.0305 60.8886 23.2248 61.1283 23.2248H62.43C62.6697 23.2248 62.8639 23.0305 62.8639 22.7909C62.8639 22.5512 62.6697 22.3569 62.43 22.3569Z" fill="#EEEEEE"/>
-<path d="M45.073 27.347H44.2052C44.0854 27.347 43.9883 27.4442 43.9883 27.564C43.9883 27.6838 44.0854 27.781 44.2052 27.781H45.073C45.1929 27.781 45.29 27.6838 45.29 27.564C45.29 27.4442 45.1929 27.347 45.073 27.347Z" fill="#FDE5D8"/>
-<path d="M45.073 29.7335H44.2052C44.0854 29.7335 43.9883 29.8307 43.9883 29.9505C43.9883 30.0703 44.0854 30.1674 44.2052 30.1674H45.073C45.1929 30.1674 45.29 30.0703 45.29 29.9505C45.29 29.8307 45.1929 29.7335 45.073 29.7335Z" fill="#FDE5D8"/>
-<path d="M45.073 32.1201H44.2052C44.0854 32.1201 43.9883 32.2173 43.9883 32.3371C43.9883 32.4569 44.0854 32.554 44.2052 32.554H45.073C45.1929 32.554 45.29 32.4569 45.29 32.3371C45.29 32.2173 45.1929 32.1201 45.073 32.1201Z" fill="#FDE5D8"/>
-<path d="M50.0631 31.9032H47.6766C47.4369 31.9032 47.2427 32.0975 47.2427 32.3371C47.2427 32.5768 47.4369 32.771 47.6766 32.771H50.0631C50.3028 32.771 50.4971 32.5768 50.4971 32.3371C50.4971 32.0975 50.3028 31.9032 50.0631 31.9032Z" fill="#6B4FBB"/>
-<path d="M55.2702 36.6763H52.8836C52.644 36.6763 52.4497 36.8705 52.4497 37.1102C52.4497 37.3498 52.644 37.5441 52.8836 37.5441H55.2702C55.5098 37.5441 55.7041 37.3498 55.7041 37.1102C55.7041 36.8705 55.5098 36.6763 55.2702 36.6763Z" fill="#EEEEEE"/>
-<path opacity="0.5" d="M58.3078 34.2897H55.9212C55.6816 34.2897 55.4873 34.4839 55.4873 34.7236C55.4873 34.9632 55.6816 35.1575 55.9212 35.1575H58.3078C58.5474 35.1575 58.7417 34.9632 58.7417 34.7236C58.7417 34.4839 58.5474 34.2897 58.3078 34.2897Z" fill="#6B4FBB"/>
-<path d="M51.1479 36.6763H47.6766C47.4369 36.6763 47.2427 36.8705 47.2427 37.1102C47.2427 37.3498 47.4369 37.5441 47.6766 37.5441H51.1479C51.3876 37.5441 51.5819 37.3498 51.5819 37.1102C51.5819 36.8705 51.3876 36.6763 51.1479 36.6763Z" fill="#EEEEEE"/>
-<path d="M54.1855 34.2897H50.7142C50.4745 34.2897 50.2803 34.4839 50.2803 34.7236C50.2803 34.9632 50.4745 35.1575 50.7142 35.1575H54.1855C54.4252 35.1575 54.6195 34.9632 54.6195 34.7236C54.6195 34.4839 54.4252 34.2897 54.1855 34.2897Z" fill="#6B4FBB"/>
-<path d="M53.1004 31.9032H51.7987C51.559 31.9032 51.3647 32.0975 51.3647 32.3371C51.3647 32.5768 51.559 32.771 51.7987 32.771H53.1004C53.3401 32.771 53.5343 32.5768 53.5343 32.3371C53.5343 32.0975 53.3401 31.9032 53.1004 31.9032Z" fill="#EEEEEE"/>
-<path d="M61.3451 34.2897H60.0433C59.8036 34.2897 59.6094 34.4839 59.6094 34.7236C59.6094 34.9632 59.8036 35.1575 60.0433 35.1575H61.3451C61.5847 35.1575 61.779 34.9632 61.779 34.7236C61.779 34.4839 61.5847 34.2897 61.3451 34.2897Z" fill="#EEEEEE"/>
-<path d="M48.9783 34.2897H47.6766C47.4369 34.2897 47.2427 34.4839 47.2427 34.7236C47.2427 34.9632 47.4369 35.1575 47.6766 35.1575H48.9783C49.218 35.1575 49.4123 34.9632 49.4123 34.7236C49.4123 34.4839 49.218 34.2897 48.9783 34.2897Z" fill="#EEEEEE"/>
-<path d="M45.073 34.5067H44.2052C44.0854 34.5067 43.9883 34.6039 43.9883 34.7237C43.9883 34.8435 44.0854 34.9406 44.2052 34.9406H45.073C45.1929 34.9406 45.29 34.8435 45.29 34.7237C45.29 34.6039 45.1929 34.5067 45.073 34.5067Z" fill="#FDE5D8"/>
-<path d="M45.073 36.8932H44.2052C44.0854 36.8932 43.9883 36.9903 43.9883 37.1101C43.9883 37.23 44.0854 37.3271 44.2052 37.3271H45.073C45.1929 37.3271 45.29 37.23 45.29 37.1101C45.29 36.9903 45.1929 36.8932 45.073 36.8932Z" fill="#FDE5D8"/>
-<path d="M66.0312 11.2921H75.4472C76.6405 11.2921 77.6168 10.3158 77.6168 9.12254V2.83072C77.6168 1.63745 76.6405 0.661133 75.4472 0.661133H65.2502C64.0569 0.661133 63.0806 1.63745 63.0806 2.83072V11.943C63.0806 13.1363 63.7748 13.4617 64.6427 12.5939L66.0312 11.2921Z" fill="white" stroke="#FDE5D8" stroke-width="2"/>
-<path d="M72.0845 3.69861H66.4436C66.2639 3.69861 66.1182 3.84431 66.1182 4.02405C66.1182 4.20378 66.2639 4.34949 66.4436 4.34949H72.0845C72.2643 4.34949 72.41 4.20378 72.41 4.02405C72.41 3.84431 72.2643 3.69861 72.0845 3.69861Z" fill="#FDB692"/>
-<path d="M74.2541 5.65125H66.4436C66.2639 5.65125 66.1182 5.79695 66.1182 5.97668C66.1182 6.15642 66.2639 6.30212 66.4436 6.30212H74.2541C74.4339 6.30212 74.5796 6.15642 74.5796 5.97668C74.5796 5.79695 74.4339 5.65125 74.2541 5.65125Z" fill="#FDB692"/>
-<path d="M72.0845 7.60388H66.4436C66.2639 7.60388 66.1182 7.74959 66.1182 7.92932C66.1182 8.10906 66.2639 8.25476 66.4436 8.25476H72.0845C72.2643 8.25476 72.41 8.10906 72.41 7.92932C72.41 7.74959 72.2643 7.60388 72.0845 7.60388Z" fill="#FDB692"/>
-<path d="M64.1655 21.0553C65.9629 21.0553 67.4199 19.5982 67.4199 17.8009C67.4199 16.0035 65.9629 14.5465 64.1655 14.5465C62.3682 14.5465 60.9111 16.0035 60.9111 17.8009C60.9111 19.5982 62.3682 21.0553 64.1655 21.0553Z" fill="#FFF7F4" stroke="#FC6D26"/>
-<path d="M62.3867 15.1974C63.0376 16.1303 64.079 16.7161 65.2506 16.7161C65.9665 16.7161 66.6174 16.4991 67.2032 16.1303" stroke="#FC6D26" stroke-width="0.5"/>
-<path d="M62.9724 18.6687C63.1521 18.6687 63.2979 18.523 63.2979 18.3433C63.2979 18.1635 63.1521 18.0178 62.9724 18.0178C62.7927 18.0178 62.647 18.1635 62.647 18.3433C62.647 18.523 62.7927 18.6687 62.9724 18.6687Z" fill="#FC6D26"/>
-<path d="M65.3591 18.6687C65.5389 18.6687 65.6846 18.523 65.6846 18.3433C65.6846 18.1635 65.5389 18.0178 65.3591 18.0178C65.1794 18.0178 65.0337 18.1635 65.0337 18.3433C65.0337 18.523 65.1794 18.6687 65.3591 18.6687Z" fill="#FC6D26"/>
-<path d="M16.0005 39.7137C17.7978 39.7137 19.2549 38.2567 19.2549 36.4593C19.2549 34.662 17.7978 33.205 16.0005 33.205C14.2031 33.205 12.7461 34.662 12.7461 36.4593C12.7461 38.2567 14.2031 39.7137 16.0005 39.7137Z" fill="#F4F1FA" stroke="#6B4FBB"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M18.1701 34.5068H18.7559C18.1701 33.5955 17.1721 32.988 16.0005 32.988C14.8289 32.988 13.8309 33.5955 13.2451 34.5068H13.8309L14.3733 33.8559L14.9157 34.5068L15.4581 33.8559L16.0005 34.5068L16.5429 33.8559L17.0853 34.5068L17.6277 33.8559L18.1701 34.5068Z" fill="#6B4FBB"/>
-<path d="M14.8074 37.1102C14.9871 37.1102 15.1328 36.9645 15.1328 36.7848C15.1328 36.6051 14.9871 36.4594 14.8074 36.4594C14.6276 36.4594 14.4819 36.6051 14.4819 36.7848C14.4819 36.9645 14.6276 37.1102 14.8074 37.1102Z" fill="#6B4FBB"/>
-<path d="M17.1936 37.1102C17.3733 37.1102 17.519 36.9645 17.519 36.7848C17.519 36.6051 17.3733 36.4594 17.1936 36.4594C17.0139 36.4594 16.8682 36.6051 16.8682 36.7848C16.8682 36.9645 17.0139 37.1102 17.1936 37.1102Z" fill="#6B4FBB"/>
-<path d="M13.0498 29.7336H3.63382C2.44055 29.7336 1.46423 28.7573 1.46423 27.5641V21.2722C1.46423 20.079 2.44055 19.1027 3.63382 19.1027H13.8309C15.0242 19.1027 16.0005 20.079 16.0005 21.2722V30.3845C16.0005 31.5778 15.3062 31.9032 14.4384 31.0354L13.0498 29.7336Z" fill="white" stroke="#E2DCF2" stroke-width="2"/>
-<path opacity="0.5" d="M4.82708 22.1401H10.468C10.6478 22.1401 10.7935 22.2858 10.7935 22.4656C10.7935 22.6453 10.6478 22.791 10.468 22.791H4.82708C4.64735 22.791 4.50164 22.6453 4.50164 22.4656C4.50164 22.2858 4.64735 22.1401 4.82708 22.1401Z" fill="#6B4FBB"/>
-<path opacity="0.5" d="M4.82724 24.0928H8.29859C8.47832 24.0928 8.62402 24.2385 8.62402 24.4182C8.62402 24.5979 8.47832 24.7437 8.29859 24.7437H4.82724C4.64751 24.7437 4.50181 24.5979 4.50181 24.4182C4.50181 24.2385 4.64751 24.0928 4.82724 24.0928Z" fill="#6B4FBB"/>
-<path opacity="0.5" d="M4.82724 26.0454H8.29859C8.47832 26.0454 8.62402 26.1911 8.62402 26.3708C8.62402 26.5506 8.47832 26.6963 8.29859 26.6963H4.82724C4.64751 26.6963 4.50181 26.5506 4.50181 26.3708C4.50181 26.1911 4.64751 26.0454 4.82724 26.0454Z" fill="#6B4FBB"/>
-</g>
-<defs>
-<clipPath id="clip0">
-<rect width="83.5292" height="48.8158" fill="white" transform="translate(-2.44092 0.661133)"/>
-</clipPath>
-</defs>
-</svg>
diff --git a/app/assets/images/learn_gitlab/pipeline_created.svg b/app/assets/images/learn_gitlab/pipeline_created.svg
deleted file mode 100644
index 91c716be475..00000000000
--- a/app/assets/images/learn_gitlab/pipeline_created.svg
+++ /dev/null
@@ -1,38 +0,0 @@
-<svg width="52" height="48" viewBox="0 0 52 48" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M32.1404 11.789L45.6109 14.1173C47.0988 14.3744 48.0923 15.7653 47.8299 17.2237L44.9797 33.0687C44.7173 34.5272 43.2985 35.5011 41.8106 35.2439L28.34 32.9157C26.8521 32.6585 25.8586 31.2677 26.121 29.8092L28.9712 13.9642C29.2336 12.5057 30.6525 11.5318 32.1404 11.789Z" fill="white"/>
-<path d="M31.9504 12.8453C31.0576 12.691 30.2063 13.2754 30.0489 14.1505L27.1986 29.9955C27.0412 30.8705 27.6373 31.705 28.5301 31.8593L42.0006 34.1876C42.8933 34.3419 43.7446 33.7576 43.9021 32.8825L46.7523 17.0375C46.9097 16.1624 46.3136 15.3279 45.4209 15.1736L31.9504 12.8453ZM32.1404 11.789L45.6109 14.1173C47.0988 14.3744 48.0923 15.7653 47.8299 17.2237L44.9797 33.0687C44.7173 34.5272 43.2985 35.5011 41.8106 35.2439L28.34 32.9157C26.8521 32.6585 25.8586 31.2677 26.121 29.8092L28.9712 13.9642C29.2336 12.5057 30.6525 11.5318 32.1404 11.789Z" fill="#E1D8F9"/>
-<path d="M39.2265 9.7425L25.6003 8.57392C24.0951 8.44485 22.7683 9.53622 22.6366 11.0116L21.206 27.0398C21.0743 28.5151 22.1877 29.8158 23.6928 29.9449L37.3191 31.1134C38.8242 31.2425 40.1511 30.1511 40.2828 28.6758L41.7133 12.6476C41.845 11.1722 40.7316 9.87157 39.2265 9.7425Z" fill="black" fill-opacity="0.03"/>
-<path d="M34.477 47.7322H41.0349C46.4986 47.5866 50.8833 43.2004 50.8833 37.8104C50.8833 32.3306 46.3517 27.8886 40.7614 27.8886C40.4897 27.8886 40.2205 27.8993 39.9544 27.9197C38.6002 24.003 34.8187 21.1847 30.3659 21.1847C28.9201 21.1826 27.4908 21.4853 26.1748 22.0723C24.3556 19.4336 21.2753 17.6986 17.7818 17.6986C12.1915 17.6986 7.65986 22.1406 7.65986 27.6204C7.65986 27.8081 7.66533 27.995 7.67572 28.1803C3.26748 29.253 0 33.1573 0 37.8104C0 43.2902 4.53163 47.7322 10.122 47.7322C10.2106 47.7322 10.2987 47.7311 10.3868 47.729L34.477 47.7322Z" fill="#F4F0FF"/>
-<path d="M40.1486 9.42187L26.547 8.27441C25.0446 8.14767 23.718 9.24259 23.5839 10.72L22.1275 26.7704C21.9934 28.2478 23.1027 29.5482 24.6051 29.6749L38.2068 30.8224C39.7092 30.9491 41.0358 29.8542 41.1698 28.3768L42.6262 12.3264C42.7603 10.849 41.651 9.54862 40.1486 9.42187Z" fill="white"/>
-<path d="M26.4499 9.34444C25.5485 9.26839 24.7525 9.92534 24.6721 10.8118L23.2156 26.8622C23.1352 27.7486 23.8007 28.5288 24.7022 28.6049L38.3038 29.7523C39.2053 29.8284 40.0013 29.1714 40.0817 28.285L41.5381 12.2346C41.6185 11.3482 40.953 10.5679 40.0516 10.4919L26.4499 9.34444ZM26.547 8.27441L40.1486 9.42187C41.651 9.54862 42.7603 10.849 42.6262 12.3264L41.1698 28.3768C41.0358 29.8542 39.7092 30.9491 38.2068 30.8224L24.6051 29.6749C23.1027 29.5482 21.9934 28.2478 22.1275 26.7704L23.5839 10.72C23.718 9.24259 25.0446 8.14767 26.547 8.27441Z" fill="#E1D8F9"/>
-<path d="M28.8145 13.3182L27.7253 13.2263C27.4245 13.2009 27.1589 13.4202 27.1321 13.7161C27.1052 14.012 27.3273 14.2725 27.6281 14.2979L28.7173 14.3898C29.018 14.4152 29.2836 14.1959 29.3105 13.9C29.3373 13.6041 29.1152 13.3436 28.8145 13.3182Z" fill="#FC6D26"/>
-<path d="M33.2417 19.0816L32.1525 18.9897C31.8517 18.9643 31.5862 19.1836 31.5593 19.4795C31.5325 19.7755 31.7545 20.0359 32.0553 20.0613L33.1445 20.1533C33.4453 20.1787 33.7109 19.9593 33.7377 19.6634C33.7645 19.3675 33.5425 19.107 33.2417 19.0816Z" fill="#E1DBF1"/>
-<path d="M36.5464 22.0571L35.4572 21.9652C35.1564 21.9398 34.8908 22.1591 34.864 22.455C34.8372 22.7509 35.0592 23.0114 35.36 23.0368L36.4492 23.1287C36.75 23.1541 37.0155 22.9348 37.0424 22.6389C37.0692 22.343 36.8472 22.0825 36.5464 22.0571Z" fill="#FEE1D3"/>
-<path d="M36.5127 19.3624L35.4235 19.2705C35.1227 19.2451 34.8571 19.4644 34.8303 19.7603C34.8035 20.0562 35.0255 20.3167 35.3263 20.3421L36.4155 20.434C36.7163 20.4594 36.9819 20.2401 37.0087 19.9442C37.0355 19.6483 36.8135 19.3878 36.5127 19.3624Z" fill="#FEF0E8"/>
-<path d="M33.1744 13.6912L30.9954 13.5071C30.6946 13.4817 30.4289 13.7012 30.4021 13.9975C30.3752 14.2938 30.5974 14.5545 30.8982 14.58L33.0772 14.764C33.378 14.7895 33.6437 14.5699 33.6705 14.2736C33.6974 13.9774 33.4752 13.7166 33.1744 13.6912Z" fill="#EFEDF8"/>
-<path d="M27.8257 21.3098L27.2813 21.2638C26.9806 21.2385 26.7151 21.4576 26.6882 21.7534C26.6614 22.0491 26.8834 22.3094 27.1841 22.3348L27.7286 22.3807C28.0293 22.4061 28.2948 22.1869 28.3216 21.8912C28.3484 21.5955 28.1264 21.3351 27.8257 21.3098Z" fill="#FEF0E8"/>
-<path d="M30.5513 21.5437L30.0069 21.4977C29.7062 21.4723 29.4407 21.6915 29.4138 21.9873C29.387 22.283 29.609 22.5433 29.9097 22.5687L30.4541 22.6146C30.7548 22.64 31.0203 22.4208 31.0472 22.1251C31.074 21.8293 30.852 21.569 30.5513 21.5437Z" fill="#FC6D26"/>
-<path d="M33.2764 21.7772L32.732 21.7312C32.4313 21.7059 32.1657 21.925 32.1389 22.2208C32.1121 22.5165 32.3341 22.7768 32.6348 22.8022L33.1792 22.8481C33.4799 22.8735 33.7455 22.6543 33.7723 22.3586C33.7991 22.0629 33.5771 21.8025 33.2764 21.7772Z" fill="#E1D8F9"/>
-<path d="M29.6651 16.0818L27.4861 15.8977C27.1853 15.8723 26.9196 16.0919 26.8928 16.3881C26.866 16.6844 27.0881 16.9452 27.3889 16.9706L29.5679 17.1547C29.8688 17.1801 30.1344 16.9605 30.1613 16.6643C30.1881 16.368 29.966 16.1072 29.6651 16.0818Z" fill="#E1D8F9"/>
-<path d="M37.126 24.7972L34.9471 24.6132C34.6462 24.5877 34.3806 24.8073 34.3537 25.1036C34.3269 25.3998 34.549 25.6606 34.8499 25.686L37.0288 25.8701C37.3297 25.8955 37.5953 25.676 37.6222 25.3797C37.649 25.0834 37.4269 24.8227 37.126 24.7972Z" fill="#6B4FBB"/>
-<path d="M37.8406 16.7796L31.8467 16.2719C31.5457 16.2464 31.28 16.4666 31.2531 16.7638C31.2263 17.0609 31.4485 17.3225 31.7494 17.348L37.7434 17.8558C38.0443 17.8812 38.3101 17.661 38.3369 17.3638C38.3638 17.0667 38.1416 16.8051 37.8406 16.7796Z" fill="#C3B8E3"/>
-<path d="M33.0379 24.4431L27.0439 23.9353C26.743 23.9098 26.4773 24.1301 26.4504 24.4272C26.4235 24.7244 26.6457 24.986 26.9467 25.0114L32.9407 25.5192C33.2416 25.5447 33.5073 25.3245 33.5342 25.0273C33.5611 24.7301 33.3389 24.4686 33.0379 24.4431Z" fill="#FEE1D3"/>
-<path d="M38.0793 14.1112L35.3553 13.881C35.0545 13.8556 34.7888 14.0753 34.7619 14.3717C34.7351 14.6681 34.9572 14.929 35.2581 14.9544L37.982 15.1847C38.2829 15.2101 38.5486 14.9904 38.5754 14.694C38.6023 14.3976 38.3801 14.1367 38.0793 14.1112Z" fill="#E1DBF1"/>
-<path d="M29.9723 18.7993L27.2484 18.5691C26.9475 18.5437 26.6818 18.7634 26.655 19.0598C26.6282 19.3562 26.8503 19.6171 27.1512 19.6426L29.8751 19.8728C30.176 19.8982 30.4417 19.6785 30.4685 19.3821C30.4953 19.0857 30.2732 18.8248 29.9723 18.7993Z" fill="#6B4FBB"/>
-<path d="M35.2983 48H41.8562C47.3199 47.8544 51.7046 43.4682 51.7046 38.0782C51.7046 32.5984 47.173 28.1564 41.5827 28.1564C41.311 28.1564 41.0418 28.1671 40.7757 28.1875C39.4215 24.2708 35.64 21.4525 31.1871 21.4525C29.7414 21.4504 28.3121 21.7532 26.9961 22.3401C25.1769 19.7014 22.0965 17.9664 18.6031 17.9664C13.0128 17.9664 8.48115 22.4085 8.48115 27.8882C8.48115 28.076 8.48662 28.2629 8.49701 28.4482C4.08877 29.5208 0.821289 33.4252 0.821289 38.0782C0.821289 43.558 5.35292 48 10.9432 48C11.0319 48 11.12 47.999 11.2081 47.9968L35.2983 48Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M35.2976 47.4637H41.8407C47.0193 47.3258 51.1567 43.1667 51.1567 38.0782C51.1567 32.8947 46.87 28.6927 41.5819 28.6927C41.3259 28.6927 41.0712 28.7026 40.8179 28.7222L40.3936 28.7549L40.2568 28.3596C38.9505 24.5818 35.3216 21.9887 31.1864 21.9887C29.8189 21.9868 28.4669 22.2731 27.2222 22.8283L26.8009 23.0163L26.5418 22.6406C24.7718 20.0727 21.8233 18.5027 18.6024 18.5027C13.3143 18.5027 9.02753 22.7047 9.02753 27.8882C9.02753 28.0654 9.03246 28.2422 9.04258 28.4183L9.06775 28.8616L8.62813 28.9686C4.39332 29.9986 1.36768 33.7413 1.36768 38.0782C1.36768 43.2616 5.65446 47.4637 10.9425 47.4637C11.0262 47.4637 11.1099 47.4626 11.2073 47.4604L35.2976 47.4637Z" fill="white"/>
-<path d="M41.5827 28.1564C47.1729 28.1564 51.7046 32.5985 51.7046 38.0782C51.7046 43.4542 47.3344 47.854 41.8415 48H35.2983L11.2203 47.9967C11.1169 47.999 11.0296 48 10.9432 48C5.35303 48 0.821289 43.5579 0.821289 38.0782C0.821289 33.5134 3.99981 29.542 8.49703 28.4481C8.48641 28.2631 8.48115 28.0765 8.48115 27.8882C8.48115 22.4086 13.0129 17.9664 18.6031 17.9664C21.9832 17.9664 25.1114 19.6064 26.9958 22.3403C28.3124 21.7532 29.742 21.4504 31.1871 21.4525C35.535 21.4525 39.3871 24.1716 40.7757 28.1874C41.0441 28.1668 41.3131 28.1564 41.5827 28.1564ZM41.8266 46.9276C46.7122 46.7976 50.6104 42.873 50.6104 38.0782C50.6104 33.1909 46.5685 29.229 41.5827 29.229C41.3418 29.229 41.1015 29.2383 40.8615 29.2569L40.4372 29.2896C40.1876 29.3088 39.9566 29.1596 39.8762 28.9272L39.7393 28.5318C38.501 24.9504 35.0651 22.5251 31.1863 22.5251C29.8973 22.5232 28.6228 22.7932 27.4496 23.3165L27.0283 23.5045C26.7852 23.6129 26.4979 23.5336 26.3483 23.3167L26.0892 22.9409C24.4075 20.5012 21.6186 19.0391 18.6031 19.0391C13.6172 19.0391 9.57541 23.001 9.57541 27.8882C9.57541 28.0567 9.58012 28.2233 9.58961 28.3886L9.61478 28.8319C9.62938 29.089 9.45538 29.3201 9.20034 29.3822L8.76063 29.4892C4.75075 30.4645 1.91555 34.007 1.91555 38.0782C1.91555 42.9655 5.95738 46.9274 10.9432 46.9274C11.0209 46.9274 11.1001 46.9264 11.2081 46.9242L35.2984 46.9274L41.8266 46.9276Z" fill="#E1D8F9"/>
-<path d="M19.2501 27.9907C19.8019 27.7404 20.3915 27.5821 20.9945 27.5223L21.1208 27.0649C21.3387 26.2755 22.1656 25.8045 22.9613 26.0114L23.7546 26.2177C24.1383 26.3176 24.4646 26.564 24.6618 26.9026C24.8589 27.2412 24.9107 27.6443 24.8057 28.0231L24.6794 28.4806C25.1755 28.8277 25.6031 29.2528 25.9488 29.7327L26.4143 29.6081C27.2149 29.3942 28.0346 29.8574 28.2469 30.6437L28.4563 31.4252C28.5574 31.8033 28.5015 32.2068 28.3009 32.5468C28.1003 32.8869 27.7714 33.1356 27.3867 33.2383L26.9221 33.3629C26.892 33.6548 26.8373 33.9481 26.7564 34.241C26.6755 34.5339 26.5718 34.8144 26.4475 35.0819L26.7857 35.4149C27.3682 35.9895 27.3647 36.9241 26.7779 37.5044L26.1939 38.0796C25.9113 38.3577 25.5291 38.5148 25.1313 38.5162C24.7336 38.5176 24.3529 38.3633 24.0732 38.0871L23.7343 37.7533C23.1824 38.0036 22.5929 38.1619 21.9899 38.2217L21.8636 38.6791C21.6457 39.4685 20.8188 39.9395 20.0231 39.7326L19.2298 39.5263C18.8461 39.4264 18.5198 39.18 18.3226 38.8414C18.1255 38.5028 18.0737 38.0997 18.1787 37.7209L18.3049 37.2634C17.8136 36.919 17.3846 36.4958 17.0356 36.0113L16.5701 36.1359C15.7695 36.3498 14.9498 35.8866 14.7374 35.1003L14.5281 34.3188C14.427 33.9407 14.4829 33.5372 14.6835 33.1972C14.8841 32.8571 15.2129 32.6084 15.5977 32.5057L16.0623 32.3812C16.0929 32.0847 16.1484 31.791 16.228 31.503C16.3089 31.2101 16.4126 30.9296 16.5369 30.6622L16.1986 30.3291C15.6162 29.7545 15.6197 28.8199 16.2065 28.2397L16.7905 27.6645C17.073 27.3863 17.4553 27.2292 17.853 27.2278C18.2508 27.2264 18.6314 27.3807 18.9112 27.6569L19.2501 27.9907Z" fill="white"/>
-<path d="M20.9944 27.5222L21.121 27.0643C21.3393 26.2769 22.1613 25.806 22.9613 26.0114L23.7546 26.2177C24.5536 26.4255 25.0238 27.2335 24.8057 28.0232L24.6796 28.4802C25.1712 28.8248 25.6002 29.2481 25.9491 29.7327L26.4148 29.6079C27.2147 29.3945 28.0336 29.8569 28.247 30.6437L28.4564 31.4256C28.5575 31.8038 28.5014 32.2073 28.3006 32.5474C28.0999 32.8874 27.7708 33.136 27.3868 33.2383L26.9221 33.3628C26.8918 33.6578 26.8364 33.9512 26.7564 34.241C26.6764 34.5306 26.5733 34.8113 26.4475 35.0819L26.7861 35.4156C27.3676 35.9897 27.3645 36.9229 26.7779 37.5044L26.1937 38.0798C25.911 38.358 25.5287 38.515 25.1308 38.5163C24.733 38.5175 24.3523 38.363 24.0733 38.0872L23.7342 37.7533C23.1825 38.0036 22.593 38.1619 21.99 38.2218L21.8635 38.6797C21.6451 39.4672 20.8231 39.938 20.0231 39.7326L19.2298 39.5263C18.4308 39.3185 17.9606 38.5106 18.1787 37.7209L18.3049 37.2638C17.8132 36.9192 17.3842 36.496 17.0353 36.0114L16.5696 36.1361C15.7697 36.3495 14.9509 35.8871 14.7374 35.1003L14.528 34.3184C14.4269 33.9403 14.483 33.5367 14.6838 33.1967C14.8846 32.8566 15.2136 32.608 15.5977 32.5057L16.0623 32.3813C16.0926 32.0862 16.148 31.7928 16.2279 31.5033C16.3073 31.2153 16.4106 30.9341 16.537 30.6621L16.1983 30.3284C15.6168 29.7543 15.62 28.8211 16.2065 28.2396L16.7907 27.6643C17.0734 27.3861 17.4558 27.229 17.8536 27.2278C18.2515 27.2265 18.6321 27.3811 18.9111 27.6568L19.2501 27.9906C19.8125 27.7376 20.3968 27.5816 20.9944 27.5222ZM19.3541 29.1231C19.1465 29.2172 18.9024 29.1749 18.742 29.0169L18.1356 28.4197C18.0595 28.3445 17.956 28.3025 17.8477 28.3028C17.7395 28.3032 17.6354 28.3459 17.5587 28.4214L16.976 28.9953C16.8161 29.1539 16.8152 29.4089 16.9737 29.5654L17.5783 30.1611C17.7385 30.3189 17.7816 30.5589 17.6864 30.7633L17.5285 31.1027C17.4273 31.3204 17.3445 31.5458 17.2808 31.7768C17.2166 32.0096 17.1723 32.2446 17.148 32.4805L17.1099 32.8514C17.087 33.0746 16.926 33.2615 16.706 33.3205L15.8747 33.5432C15.77 33.5711 15.6805 33.6387 15.6258 33.7312C15.5712 33.8237 15.556 33.9335 15.5834 34.0361L15.7923 34.816C15.8505 35.0306 16.0743 35.1569 16.2923 35.0987L17.124 34.876C17.3443 34.817 17.5775 34.8987 17.7088 35.0811L17.927 35.3841C18.2062 35.7718 18.5493 36.1104 18.942 36.3857L19.2494 36.6007C19.4343 36.7301 19.5173 36.9598 19.4573 37.1768L19.2315 37.9947C19.1719 38.2106 19.2997 38.4302 19.5168 38.4867L20.3085 38.6926C20.5267 38.7486 20.7513 38.6199 20.8108 38.4054L21.0366 37.5874C21.0965 37.3706 21.2864 37.212 21.5131 37.1895L21.89 37.1522C22.3723 37.1043 22.8438 36.9776 23.2856 36.7771L23.6306 36.6208C23.8382 36.5268 24.082 36.5692 24.2424 36.7271L24.8488 37.3243C24.9249 37.3995 25.0285 37.4416 25.1367 37.4412C25.2449 37.4409 25.349 37.3981 25.4257 37.3226L26.0084 36.7487C26.1683 36.5901 26.1692 36.3351 26.0107 36.1786L25.4061 35.5829C25.2459 35.4251 25.2028 35.1851 25.298 34.9807L25.456 34.6412C25.5567 34.4246 25.6393 34.1998 25.7036 33.9672C25.7678 33.7345 25.8122 33.4995 25.8364 33.2636L25.8745 32.8927C25.8974 32.6695 26.0584 32.4825 26.2784 32.4236L27.1097 32.2009C27.2144 32.173 27.3039 32.1053 27.3586 32.0128C27.4132 31.9203 27.4284 31.8105 27.401 31.708L27.1921 30.928C27.1339 30.7135 26.9101 30.5871 26.6921 30.6453L25.8604 30.868C25.6401 30.9271 25.4069 30.8453 25.2756 30.663L25.0574 30.3599C24.7783 29.9722 24.4351 29.6336 24.0427 29.3586L23.7353 29.1438C23.5501 29.0145 23.4671 28.7847 23.527 28.5675L23.7529 27.7494C23.8125 27.5334 23.6847 27.3138 23.4676 27.2573L22.6759 27.0514C22.4578 26.9954 22.2332 27.1241 22.1736 27.3387L21.9478 28.1566C21.8879 28.3735 21.698 28.5321 21.4713 28.5545L21.0944 28.5918C20.6121 28.6397 20.1406 28.7665 19.6992 28.9668C19.696 28.9682 19.5809 29.0203 19.3541 29.1231Z" fill="#7B58CF"/>
-<path d="M19.4256 36.6322C20.4661 37.2185 21.8046 36.8571 22.4152 35.825C23.0258 34.7929 22.6774 33.4808 21.6369 32.8945C20.5964 32.3082 19.2579 32.6696 18.6473 33.7017C18.0367 34.7339 18.3851 36.0459 19.4256 36.6322Z" fill="white"/>
-<path d="M20.2669 34.8677C19.2265 34.2814 18.878 32.9693 19.4886 31.9372C20.0992 30.9051 21.4377 30.5437 22.4782 31.13C23.5187 31.7163 23.8671 33.0283 23.2565 34.0605C22.6459 35.0926 21.3074 35.454 20.2669 34.8677ZM20.8198 33.9333C21.34 34.2264 22.0092 34.0457 22.3145 33.5297C22.6199 33.0136 22.4456 32.3576 21.9254 32.0644C21.4051 31.7713 20.7359 31.952 20.4306 32.468C20.1253 32.9841 20.2995 33.6401 20.8198 33.9333Z" fill="#6B4FBB"/>
-<path d="M30.889 35.0709C31.3313 34.8678 31.8035 34.7389 32.2863 34.6895L32.3883 34.3201C32.5643 33.6826 33.2272 33.3008 33.8637 33.4663L34.4983 33.6313C34.8053 33.7113 35.066 33.9096 35.2232 34.1825C35.3803 34.4555 35.421 34.7807 35.3362 35.0867L35.2342 35.4561C35.6306 35.7354 35.972 36.0777 36.2478 36.4644L36.6207 36.3629C37.262 36.1888 37.9173 36.5612 38.0857 37.1955L38.2517 37.8259C38.3319 38.1309 38.2864 38.4566 38.1251 38.7315C37.9638 39.0063 37.7001 39.2077 37.3919 39.2913L37.0198 39.3927C36.9951 39.6284 36.9507 39.8652 36.8854 40.1018C36.82 40.3384 36.7365 40.565 36.6364 40.7811L36.9065 41.0493C37.3717 41.512 37.367 42.2664 36.8961 42.7358L36.4275 43.2012C36.2007 43.4262 35.8945 43.5537 35.5761 43.5556C35.2577 43.5575 34.9532 43.4336 34.7299 43.2112L34.4592 42.9424C34.017 43.1455 33.5447 43.2744 33.0619 43.3238L32.9599 43.6932C32.7839 44.3307 32.1211 44.7125 31.4845 44.547L30.8499 44.3819C30.543 44.302 30.2822 44.1037 30.1251 43.8308C29.9679 43.5578 29.9273 43.2326 30.0121 42.9266L30.1141 42.5572C29.7214 42.2801 29.3788 41.9393 29.1004 41.5488L28.7276 41.6503C28.0863 41.8245 27.431 41.4521 27.2626 40.8178L27.0965 40.1874C27.0163 39.8824 27.0619 39.5566 27.2231 39.2818C27.3844 39.007 27.6481 38.8056 27.9563 38.722L28.3285 38.6206C28.3536 38.3812 28.3986 38.144 28.4629 37.9115C28.5282 37.6749 28.6118 37.4483 28.7118 37.2322L28.4417 36.964C27.9766 36.5013 27.9813 35.7469 28.4522 35.2775L28.9207 34.8121C29.1475 34.5871 29.4538 34.4595 29.7722 34.4577C30.0906 34.4558 30.395 34.5797 30.6184 34.802L30.889 35.0709Z" fill="white"/>
-<path d="M32.1998 34.5947L32.2833 34.2923C32.4746 33.6024 33.1919 33.1869 33.8914 33.3665L34.526 33.5315C35.2246 33.7132 35.6326 34.4223 35.4416 35.1142L35.3583 35.4159C35.7153 35.6768 36.0302 35.9898 36.2925 36.3444L36.5904 36.2633C37.2909 36.0733 38.0069 36.4799 38.1913 37.1715L38.3575 37.8023C38.4444 38.133 38.3949 38.4857 38.2201 38.7834C38.045 39.0816 37.7581 39.3004 37.423 39.3911L37.1204 39.4735C37.0938 39.6938 37.0505 39.9128 36.9908 40.1294C36.931 40.3458 36.8557 40.5563 36.7651 40.7602L36.9851 40.9788C37.4903 41.4818 37.4856 42.298 36.9752 42.8081L36.5064 43.2737C36.2596 43.5186 35.9255 43.6575 35.5775 43.6595C35.2288 43.6614 34.8954 43.5256 34.6519 43.2828L34.4357 43.0682C34.026 43.2479 33.5922 43.3661 33.1486 43.4189L33.0651 43.7214C32.8738 44.4113 32.1565 44.8267 31.457 44.6472L30.8224 44.4821C30.1238 44.3005 29.7158 43.5913 29.9068 42.8994L29.9901 42.5978C29.6331 42.3368 29.3182 42.0238 29.0559 41.6693L28.758 41.7504C28.0575 41.9403 27.3415 41.5338 27.1571 40.8421L26.9909 40.2114C26.9041 39.8807 26.9535 39.5279 27.1283 39.2303C27.3034 38.9321 27.5903 38.7132 27.9254 38.6225L28.228 38.5401C28.2546 38.3199 28.2979 38.1009 28.3576 37.8845C28.417 37.6691 28.4924 37.4583 28.5833 37.2535L28.3633 37.0348C27.8581 36.5319 27.8628 35.7157 28.3732 35.2055L28.842 34.74C29.0888 34.4951 29.4229 34.3561 29.7709 34.3542C30.1196 34.3522 30.453 34.4881 30.6965 34.7308L30.9127 34.9455C31.3224 34.7658 31.7562 34.6475 32.1998 34.5947ZM31.0182 36.0782C30.8094 36.1741 30.5632 36.1315 30.4022 35.9716L29.9178 35.4906C29.878 35.4508 29.8241 35.4289 29.7676 35.4292C29.7105 35.4295 29.6548 35.4527 29.6133 35.4939L29.1459 35.958C29.057 36.0469 29.0562 36.1897 29.1419 36.275L29.6246 36.7547C29.7833 36.9124 29.8259 37.151 29.7315 37.3546L29.6044 37.6287C29.5253 37.7997 29.4605 37.9767 29.4105 38.1581C29.36 38.3408 29.3251 38.5252 29.3058 38.7103L29.2745 39.0097C29.2514 39.2318 29.0916 39.4178 28.8729 39.4774L28.2068 39.6588C28.1506 39.674 28.1018 39.7112 28.0716 39.7626C28.0411 39.8146 28.0325 39.8763 28.0474 39.9333L28.2131 40.5621C28.2441 40.6786 28.3616 40.7453 28.4764 40.7142L29.1425 40.5328C29.3644 40.4725 29.5996 40.555 29.731 40.7394L29.9051 40.9836C30.1208 41.2862 30.3862 41.5502 30.6899 41.7646L30.9355 41.9376C31.1195 42.0672 31.2018 42.2962 31.142 42.5126L30.9597 43.1732C30.9264 43.2936 30.995 43.4128 31.1094 43.4425L31.7424 43.6071C31.8578 43.6367 31.9794 43.5663 32.0125 43.447L32.1949 42.7864C32.2546 42.5701 32.4437 42.4117 32.6697 42.3887L32.9714 42.3578C33.3452 42.3195 33.711 42.2196 34.054 42.0621L34.3305 41.9353C34.5392 41.8396 34.7853 41.8823 34.9462 42.0421L35.4306 42.5231C35.4704 42.5628 35.5243 42.5848 35.5808 42.5845C35.6379 42.5841 35.6936 42.561 35.7352 42.5197L36.2025 42.0556C36.2914 41.9668 36.2922 41.8239 36.2065 41.7387L35.7238 41.259C35.5651 41.1013 35.5225 40.8626 35.6169 40.6591L35.7441 40.3847C35.8228 40.2147 35.8875 40.0382 35.9379 39.8556C35.9884 39.6728 36.0233 39.4884 36.0427 39.3034L36.0739 39.0039C36.097 38.7819 36.2568 38.5958 36.4755 38.5363L37.1416 38.3549C37.1978 38.3397 37.2466 38.3025 37.2768 38.2511C37.3073 38.1991 37.3159 38.1373 37.301 38.0803L37.1353 37.4515C37.1043 37.335 36.9868 37.2683 36.872 37.2995L36.2059 37.4808C35.9841 37.5412 35.7488 37.4586 35.6174 37.2743L35.4433 37.0301C35.2276 36.7274 34.9622 36.4634 34.6588 36.2493L34.4132 36.0765C34.2289 35.947 34.1465 35.7178 34.2063 35.5012L34.3888 34.8404C34.422 34.7201 34.3534 34.6009 34.239 34.5711L33.606 34.4065C33.4907 34.3769 33.369 34.4473 33.3359 34.5666L33.1535 35.2272C33.0938 35.4435 32.9047 35.6019 32.6788 35.625L32.377 35.6558C32.0032 35.6941 31.6374 35.794 31.2948 35.9514C31.2919 35.9528 31.1997 35.995 31.0182 36.0782Z" fill="#FC6D26"/>
-<path d="M31.0067 42.0443C31.839 42.5134 32.9136 42.2178 33.4068 41.3841C33.9 40.5504 33.6251 39.4944 32.7928 39.0253C31.9604 38.5563 30.8858 38.8519 30.3926 39.6855C29.8994 40.5192 30.1743 41.5753 31.0067 42.0443Z" fill="white"/>
-<path d="M31.6893 40.6162C30.8561 40.1466 30.5815 39.0919 31.073 38.2611C31.5645 37.4302 32.6378 37.135 33.471 37.6045C34.3042 38.074 34.5788 39.1288 34.0873 39.9596C33.5957 40.7905 32.5225 41.0857 31.6893 40.6162ZM32.1315 39.8686C32.5469 40.1027 33.0855 39.9546 33.3337 39.535C33.5819 39.1154 33.4441 38.5861 33.0288 38.3521C32.6134 38.118 32.0748 38.2661 31.8266 38.6857C31.5783 39.1053 31.7162 39.6346 32.1315 39.8686Z" fill="#FC6D26"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M16.8736 10.2573C16.9313 9.88553 16.9613 9.50478 16.9613 9.11717C16.9613 4.97039 13.5319 1.60876 9.30146 1.60876C5.07104 1.60876 1.6416 4.97039 1.6416 9.11717C1.6416 13.2639 5.07104 16.6256 9.30146 16.6256C11.7125 16.6256 13.8633 15.5337 15.2674 13.8269L17.582 13.5753C17.637 13.5693 17.6908 13.5552 17.7414 13.5334C18.0182 13.4145 18.1442 13.0982 18.0228 12.8269L16.8736 10.2573Z" fill="#F4F0FF"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M17.42 9.18492C17.4777 8.81314 17.5077 8.43239 17.5077 8.04478C17.5077 3.898 14.0783 0.536377 9.84784 0.536377C5.61742 0.536377 2.18799 3.898 2.18799 8.04478C2.18799 12.1916 5.61742 15.5532 9.84784 15.5532C12.2588 15.5532 14.4097 14.4613 15.8138 12.7545L18.1284 12.5029C18.1834 12.4969 18.2371 12.4828 18.2878 12.461C18.5646 12.3421 18.6905 12.0258 18.5692 11.7545L17.42 9.18492Z" fill="white"/>
-<path d="M19.0711 11.5391C19.3137 12.0817 19.0617 12.7143 18.5082 12.9522C18.4069 12.9957 18.2994 13.0239 18.1895 13.0359L16.0945 13.2636C14.5478 15.0412 12.2855 16.0894 9.84859 16.0894C5.31599 16.0894 1.6416 12.4877 1.6416 8.04471C1.6416 3.60174 5.31599 0 9.84859 0C14.3812 0 18.0556 3.60174 18.0556 8.04471C18.0556 8.40289 18.0317 8.75827 17.9843 9.10926L19.0711 11.5391ZM16.8798 9.10409C16.9339 8.75583 16.9613 8.40206 16.9613 8.04471C16.9613 4.19414 13.7768 1.07263 9.84859 1.07263C5.92034 1.07263 2.73587 4.19414 2.73587 8.04471C2.73587 11.8953 5.92034 15.0168 9.84859 15.0168C12.0288 15.0168 14.0459 14.0499 15.3886 12.4178L15.5301 12.2457L18.0689 11.9698L16.9196 9.4002L16.8559 9.2578L16.8798 9.10409Z" fill="#E1D8F9"/>
-<path d="M9.98471 11.9331C7.8695 11.9331 6.15479 10.2523 6.15479 8.17888C6.15479 6.1055 7.8695 4.42468 9.98471 4.42468C12.0999 4.42468 13.8146 6.1055 13.8146 8.17888C13.8146 10.2523 12.0999 11.9331 9.98471 11.9331ZM9.98471 11.1286C11.6467 11.1286 12.9939 9.80798 12.9939 8.17888C12.9939 6.54979 11.6467 5.22915 9.98471 5.22915C8.32276 5.22915 6.97548 6.54979 6.97548 8.17888C6.97548 9.80798 8.32276 11.1286 9.98471 11.1286Z" fill="#31AF64"/>
-<path d="M9.62376 8.52988L9.09523 8.0118C8.93843 7.85825 8.68438 7.85825 8.52758 8.0118C8.4521 8.08542 8.40967 8.1855 8.40967 8.28987C8.40967 8.39425 8.4521 8.49433 8.52758 8.56795L9.31408 9.33864C9.31836 9.34303 9.32274 9.34732 9.32721 9.35151C9.47767 9.499 9.71841 9.49873 9.86751 9.35285L11.4564 7.79539C11.5279 7.725 11.5679 7.62964 11.5677 7.53031C11.5674 7.43098 11.5269 7.33582 11.455 7.26578C11.3837 7.19514 11.2866 7.15528 11.1851 7.15503C11.0837 7.15478 10.9864 7.19415 10.9147 7.26444L9.62376 8.52988Z" fill="#31AF64"/>
-</svg>
diff --git a/app/assets/images/learn_gitlab/required_mr_approvals_enabled.svg b/app/assets/images/learn_gitlab/required_mr_approvals_enabled.svg
deleted file mode 100644
index 027767368a6..00000000000
--- a/app/assets/images/learn_gitlab/required_mr_approvals_enabled.svg
+++ /dev/null
@@ -1,70 +0,0 @@
-<svg width="80" height="56" viewBox="0 0 80 56" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M11.1567 40.6485C16.4079 40.6485 20.6649 36.3891 20.6649 31.135C20.6649 25.8808 16.4079 21.6215 11.1567 21.6215C5.9055 21.6215 1.64856 25.8808 1.64856 31.135C1.64856 36.3891 5.9055 40.6485 11.1567 40.6485Z" fill="#F9F9F9"/>
-<path d="M19.0163 30.4864C19.0163 35.1888 15.2065 38.9999 10.5081 38.9999C5.80976 38.9999 2 35.1888 2 30.4864C2 25.784 5.80976 21.9729 10.5081 21.9729C15.2065 21.9729 19.0163 25.784 19.0163 30.4864Z" fill="white" stroke="#EEEEEE" stroke-width="2"/>
-<path d="M10.5075 39.5674C15.52 39.5674 19.5834 35.5017 19.5834 30.4863C19.5834 25.471 15.52 21.4053 10.5075 21.4053C5.49496 21.4053 1.43152 25.471 1.43152 30.4863C1.43152 35.5017 5.49496 39.5674 10.5075 39.5674Z" stroke="#EEEEEE"/>
-<path d="M8.43196 33.0239C8.57802 32.9204 8.75259 32.8648 8.93158 32.8648H13.1009C13.817 32.8648 14.3975 32.284 14.3975 31.5675V28.5405C14.3975 27.824 13.817 27.2432 13.1009 27.2432H8.34686C7.63078 27.2432 7.05029 27.824 7.05029 28.5405V34.0031L8.43196 33.0239ZM8.93158 33.7296L7.20891 34.9505C7.09937 35.0282 6.96844 35.0699 6.8342 35.0699C6.47616 35.0699 6.18591 34.7795 6.18591 34.4212V28.5405C6.18591 27.3463 7.1534 26.3783 8.34686 26.3783H13.1009C14.2944 26.3783 15.2619 27.3463 15.2619 28.5405V31.5675C15.2619 32.7616 14.2944 33.7296 13.1009 33.7296H8.93158Z" fill="#FEE1D3"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M9.21191 29.6217C9.4506 29.6217 9.6441 29.8153 9.6441 30.0541C9.6441 30.293 9.4506 30.4866 9.21191 30.4866C8.97322 30.4866 8.77972 30.293 8.77972 30.0541C8.77972 29.8153 8.97322 29.6217 9.21191 29.6217ZM10.7246 29.6217C10.9633 29.6217 11.1568 29.8153 11.1568 30.0541C11.1568 30.293 10.9633 30.4866 10.7246 30.4866C10.4859 30.4866 10.2924 30.293 10.2924 30.0541C10.2924 29.8153 10.4859 29.6217 10.7246 29.6217ZM12.2372 29.6217C12.4759 29.6217 12.6694 29.8153 12.6694 30.0541C12.6694 30.293 12.4759 30.4866 12.2372 30.4866C11.9985 30.4866 11.805 30.293 11.805 30.0541C11.805 29.8153 11.9985 29.6217 12.2372 29.6217Z" fill="#FC6D26"/>
-<path d="M18.0716 14.0539C21.7713 14.0539 24.7705 11.0531 24.7705 7.35125C24.7705 3.64946 21.7713 0.64856 18.0716 0.64856C14.3719 0.64856 11.3727 3.64946 11.3727 7.35125C11.3727 11.0531 14.3719 14.0539 18.0716 14.0539Z" fill="#F9F9F9"/>
-<path d="M17.423 13.4054C21.1228 13.4054 24.122 10.4045 24.122 6.70269C24.122 3.0009 21.1228 0 17.423 0C13.7233 0 10.7241 3.0009 10.7241 6.70269C10.7241 10.4045 13.7233 13.4054 17.423 13.4054Z" fill="white"/>
-<path d="M19.1525 6.27026H15.6949C15.4563 6.27026 15.2628 6.46387 15.2628 6.7027C15.2628 6.94152 15.4563 7.13513 15.6949 7.13513H19.1525C19.3911 7.13513 19.5846 6.94152 19.5846 6.7027C19.5846 6.46387 19.3911 6.27026 19.1525 6.27026Z" fill="#6B4FBB"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M49.7467 48.0001H20.8808C19.4486 48.0001 18.2877 46.8384 18.2877 45.4055V12.5406C18.2877 11.1077 19.4486 9.94604 20.8808 9.94604H65.3962C66.8284 9.94604 67.9893 11.1077 67.9893 12.5406V16.5187C69.077 15.9128 70.3297 15.5677 71.6629 15.5677C75.84 15.5677 79.2262 18.9558 79.2262 23.1352C79.2262 27.3147 75.84 30.7028 71.6629 30.7028C70.3297 30.7028 69.077 30.3576 67.9893 29.7517V45.4055C67.9893 46.8384 66.8284 48.0001 65.3962 48.0001H62.8938C62.9761 48.4198 63.0192 48.8535 63.0192 49.2974C63.0192 52.9991 60.02 56 56.3202 56C52.6205 56 49.6213 52.9991 49.6213 49.2974C49.6213 48.8535 49.6644 48.4198 49.7467 48.0001Z" fill="#F9F9F9"/>
-<path d="M71.0139 30.0543C75.191 30.0543 78.5772 26.6662 78.5772 22.4868C78.5772 18.3073 75.191 14.9192 71.0139 14.9192C66.8368 14.9192 63.4506 18.3073 63.4506 22.4868C63.4506 26.6662 66.8368 30.0543 71.0139 30.0543Z" fill="white"/>
-<path d="M71.0148 29.6218C74.9532 29.6218 78.1459 26.4273 78.1459 22.4867C78.1459 18.5461 74.9532 15.3516 71.0148 15.3516C67.0764 15.3516 63.8837 18.5461 63.8837 22.4867C63.8837 26.4273 67.0764 29.6218 71.0148 29.6218Z" stroke="#EEEEEE" stroke-width="2"/>
-<path d="M71.014 25.7303C72.8042 25.7303 74.2554 24.2782 74.2554 22.487C74.2554 20.6958 72.8042 19.2438 71.014 19.2438C69.2238 19.2438 67.7726 20.6958 67.7726 22.487C67.7726 24.2782 69.2238 25.7303 71.014 25.7303Z" fill="#F4F1FA" stroke="#6B4FBB"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M71.9682 20.5401L71.9667 20.5401L71.4366 19.8925L70.9063 20.5404L70.9051 20.5404L70.3748 19.8925L69.8443 20.5407L69.8436 20.5407L69.3131 19.8925L68.7823 20.541L68.2041 20.5411C68.7671 19.6321 69.7666 19.0276 70.9057 19.0276C72.0443 19.0276 73.0433 19.6315 73.6065 20.5397L73.0282 20.5399L72.4983 19.8925L71.9682 20.5401Z" fill="#6B4FBB"/>
-<path d="M69.8258 23.1357C70.0048 23.1357 70.1499 22.9905 70.1499 22.8114C70.1499 22.6323 70.0048 22.4871 69.8258 22.4871C69.6468 22.4871 69.5016 22.6323 69.5016 22.8114C69.5016 22.9905 69.6468 23.1357 69.8258 23.1357Z" fill="#6B4FBB"/>
-<path d="M72.2021 23.1357C72.3811 23.1357 72.5263 22.9905 72.5263 22.8114C72.5263 22.6323 72.3811 22.4871 72.2021 22.4871C72.0231 22.4871 71.878 22.6323 71.878 22.8114C71.878 22.9905 72.0231 23.1357 72.2021 23.1357Z" fill="#6B4FBB"/>
-<path d="M65.1797 9.29712H19.7999C18.6064 9.29712 17.6389 10.2652 17.6389 11.4593V45.189C17.6389 46.3831 18.6064 47.3511 19.7999 47.3511H65.1797C66.3731 47.3511 67.3406 46.3831 67.3406 45.189V11.4593C67.3406 10.2652 66.3731 9.29712 65.1797 9.29712Z" fill="white"/>
-<path d="M64.7466 9.72986H20.2311C19.0377 9.72986 18.0702 10.6979 18.0702 11.892V44.7568C18.0702 45.951 19.0377 46.919 20.2311 46.919H64.7466C65.94 46.919 66.9075 45.951 66.9075 44.7568V11.892C66.9075 10.6979 65.94 9.72986 64.7466 9.72986Z" stroke="#EEEEEE" stroke-width="2"/>
-<path d="M25.6338 18.8108C25.6338 18.572 25.4403 18.3784 25.2016 18.3784C24.9629 18.3784 24.7694 18.572 24.7694 18.8108V44.3243C24.7694 44.5632 24.9629 44.7568 25.2016 44.7568C25.4403 44.7568 25.6338 44.5632 25.6338 44.3243V18.8108Z" fill="#EEEEEE"/>
-<path d="M22.177 22.0541H20.8804C20.6417 22.0541 20.4482 22.2477 20.4482 22.4865C20.4482 22.7253 20.6417 22.9189 20.8804 22.9189H22.177C22.4157 22.9189 22.6092 22.7253 22.6092 22.4865C22.6092 22.2477 22.4157 22.0541 22.177 22.0541Z" fill="#FEE1D3"/>
-<path d="M22.177 24.6486H20.8804C20.6417 24.6486 20.4482 24.8422 20.4482 25.081C20.4482 25.3198 20.6417 25.5134 20.8804 25.5134H22.177C22.4157 25.5134 22.6092 25.3198 22.6092 25.081C22.6092 24.8422 22.4157 24.6486 22.177 24.6486Z" fill="#F0EDF8"/>
-<path d="M22.177 27.2432H20.8804C20.6417 27.2432 20.4482 27.4368 20.4482 27.6756C20.4482 27.9144 20.6417 28.108 20.8804 28.108H22.177C22.4157 28.108 22.6092 27.9144 22.6092 27.6756C22.6092 27.4368 22.4157 27.2432 22.177 27.2432Z" fill="#FEF0E9"/>
-<path d="M22.177 29.8379H20.8804C20.6417 29.8379 20.4482 30.0315 20.4482 30.2703C20.4482 30.5091 20.6417 30.7028 20.8804 30.7028H22.177C22.4157 30.7028 22.6092 30.5091 22.6092 30.2703C22.6092 30.0315 22.4157 29.8379 22.177 29.8379Z" fill="#FEE1D3"/>
-<path d="M22.177 32.4324H20.8804C20.6417 32.4324 20.4482 32.626 20.4482 32.8648C20.4482 33.1036 20.6417 33.2972 20.8804 33.2972H22.177C22.4157 33.2972 22.6092 33.1036 22.6092 32.8648C22.6092 32.626 22.4157 32.4324 22.177 32.4324Z" fill="#E1DBF1"/>
-<path d="M22.177 35.027H20.8804C20.6417 35.027 20.4482 35.2206 20.4482 35.4594C20.4482 35.6982 20.6417 35.8918 20.8804 35.8918H22.177C22.4157 35.8918 22.6092 35.6982 22.6092 35.4594C22.6092 35.2206 22.4157 35.027 22.177 35.027Z" fill="#F0EDF8"/>
-<path d="M22.177 37.6216H20.8804C20.6417 37.6216 20.4482 37.8152 20.4482 38.054C20.4482 38.2928 20.6417 38.4864 20.8804 38.4864H22.177C22.4157 38.4864 22.6092 38.2928 22.6092 38.054C22.6092 37.8152 22.4157 37.6216 22.177 37.6216Z" fill="#FEF0E9"/>
-<path d="M22.177 40.2161H20.8804C20.6417 40.2161 20.4482 40.4097 20.4482 40.6485C20.4482 40.8873 20.6417 41.0809 20.8804 41.0809H22.177C22.4157 41.0809 22.6092 40.8873 22.6092 40.6485C22.6092 40.4097 22.4157 40.2161 22.177 40.2161Z" fill="#FEE1D3"/>
-<path d="M32.1164 22.0538H29.9555C29.7168 22.0538 29.5233 22.2474 29.5233 22.4863C29.5233 22.7251 29.7168 22.9187 29.9555 22.9187H32.1164C32.3551 22.9187 32.5486 22.7251 32.5486 22.4863C32.5486 22.2474 32.3551 22.0538 32.1164 22.0538Z" fill="#6B4FBB"/>
-<path d="M36.439 22.0537H34.278C34.0393 22.0537 33.8458 22.2473 33.8458 22.4861C33.8458 22.725 34.0393 22.9186 34.278 22.9186H36.439C36.6776 22.9186 36.8711 22.725 36.8711 22.4861C36.8711 22.2473 36.6776 22.0537 36.439 22.0537Z" fill="#F0EDF8"/>
-<path d="M40.7601 22.0537H38.5992C38.3605 22.0537 38.167 22.2473 38.167 22.4861C38.167 22.725 38.3605 22.9186 38.5992 22.9186H40.7601C40.9988 22.9186 41.1923 22.725 41.1923 22.4861C41.1923 22.2473 40.9988 22.0537 40.7601 22.0537Z" fill="#FEF0E9"/>
-<path d="M32.1161 24.6484H29.9551C29.7164 24.6484 29.5229 24.842 29.5229 25.0809C29.5229 25.3197 29.7164 25.5133 29.9551 25.5133H32.1161C32.3548 25.5133 32.5483 25.3197 32.5483 25.0809C32.5483 24.842 32.3548 24.6484 32.1161 24.6484Z" fill="#F0EDF8"/>
-<path d="M40.7601 27.243H38.5992C38.3605 27.243 38.167 27.4366 38.167 27.6755C38.167 27.9143 38.3605 28.1079 38.5992 28.1079H40.7601C40.9988 28.1079 41.1923 27.9143 41.1923 27.6755C41.1923 27.4366 40.9988 27.243 40.7601 27.243Z" fill="#FEF0E9"/>
-<path d="M32.1161 32.4323H29.9551C29.7164 32.4323 29.5229 32.6259 29.5229 32.8647C29.5229 33.1035 29.7164 33.2971 29.9551 33.2971H32.1161C32.3548 33.2971 32.5483 33.1035 32.5483 32.8647C32.5483 32.6259 32.3548 32.4323 32.1161 32.4323Z" fill="#E1DBF1"/>
-<path d="M40.7601 29.8378H38.5992C38.3605 29.8378 38.167 30.0314 38.167 30.2702C38.167 30.509 38.3605 30.7026 38.5992 30.7026H40.7601C40.9988 30.7026 41.1923 30.509 41.1923 30.2702C41.1923 30.0314 40.9988 29.8378 40.7601 29.8378Z" fill="#FEF0E9"/>
-<path d="M34.9263 24.6484H34.278C34.0393 24.6484 33.8458 24.842 33.8458 25.0809C33.8458 25.3197 34.0393 25.5133 34.278 25.5133H34.9263C35.165 25.5133 35.3585 25.3197 35.3585 25.0809C35.3585 24.842 35.165 24.6484 34.9263 24.6484Z" fill="#FEE1D3"/>
-<path d="M36.4379 29.8378H35.7896C35.5509 29.8378 35.3574 30.0314 35.3574 30.2702C35.3574 30.509 35.5509 30.7026 35.7896 30.7026H36.4379C36.6766 30.7026 36.8701 30.509 36.8701 30.2702C36.8701 30.0314 36.6766 29.8378 36.4379 29.8378Z" fill="#6B4FBB"/>
-<path d="M34.9263 32.4323H34.278C34.0393 32.4323 33.8458 32.6259 33.8458 32.8647C33.8458 33.1035 34.0393 33.2971 34.278 33.2971H34.9263C35.165 33.2971 35.3585 33.1035 35.3585 32.8647C35.3585 32.6259 35.165 32.4323 34.9263 32.4323Z" fill="#FEE1D3"/>
-<path d="M30.6034 27.243H29.9551C29.7164 27.243 29.5229 27.4366 29.5229 27.6755C29.5229 27.9143 29.7164 28.1079 29.9551 28.1079H30.6034C30.8421 28.1079 31.0356 27.9143 31.0356 27.6755C31.0356 27.4366 30.8421 27.243 30.6034 27.243Z" fill="#FC6D26"/>
-<path d="M36.4383 27.243H32.7647C32.526 27.243 32.3325 27.4366 32.3325 27.6755C32.3325 27.9143 32.526 28.1079 32.7647 28.1079H36.4383C36.677 28.1079 36.8705 27.9143 36.8705 27.6755C36.8705 27.4366 36.677 27.243 36.4383 27.243Z" fill="#E1DBF1"/>
-<path d="M33.6287 29.8378H29.9551C29.7164 29.8378 29.5229 30.0314 29.5229 30.2702C29.5229 30.509 29.7164 30.7026 29.9551 30.7026H33.6287C33.8674 30.7026 34.0609 30.509 34.0609 30.2702C34.0609 30.0314 33.8674 29.8378 33.6287 29.8378Z" fill="#EEEEEE"/>
-<path d="M37.7342 24.6484H37.0859C36.8472 24.6484 36.6537 24.842 36.6537 25.0809C36.6537 25.3197 36.8472 25.5133 37.0859 25.5133H37.7342C37.9728 25.5133 38.1663 25.3197 38.1663 25.0809C38.1663 24.842 37.9728 24.6484 37.7342 24.6484Z" fill="#6B4FBB"/>
-<path d="M53.2933 22.054H51.1323C50.8936 22.054 50.7001 22.2476 50.7001 22.4864C50.7001 22.7252 50.8936 22.9188 51.1323 22.9188H53.2933C53.532 22.9188 53.7254 22.7252 53.7254 22.4864C53.7254 22.2476 53.532 22.054 53.2933 22.054Z" fill="#FEE1D3"/>
-<path d="M57.6143 22.054H55.4533C55.2146 22.054 55.0211 22.2476 55.0211 22.4864C55.0211 22.7252 55.2146 22.9188 55.4533 22.9188H57.6143C57.8529 22.9188 58.0464 22.7252 58.0464 22.4864C58.0464 22.2476 57.8529 22.054 57.6143 22.054Z" fill="#F0EDF8"/>
-<path d="M61.9367 22.054H59.7758C59.5371 22.054 59.3436 22.2476 59.3436 22.4864C59.3436 22.7252 59.5371 22.9188 59.7758 22.9188H61.9367C62.1754 22.9188 62.3689 22.7252 62.3689 22.4864C62.3689 22.2476 62.1754 22.054 61.9367 22.054Z" fill="#FC6D26"/>
-<path d="M53.2931 24.6486H51.1321C50.8934 24.6486 50.7 24.8422 50.7 25.081C50.7 25.3198 50.8934 25.5134 51.1321 25.5134H53.2931C53.5318 25.5134 53.7253 25.3198 53.7253 25.081C53.7253 24.8422 53.5318 24.6486 53.2931 24.6486Z" fill="#FEF0E9"/>
-<path d="M61.9367 27.2432H59.7758C59.5371 27.2432 59.3436 27.4368 59.3436 27.6756C59.3436 27.9144 59.5371 28.108 59.7758 28.108H61.9367C62.1754 28.108 62.3689 27.9144 62.3689 27.6756C62.3689 27.4368 62.1754 27.2432 61.9367 27.2432Z" fill="#E1DBF1"/>
-<path d="M53.2931 32.4324H51.1321C50.8934 32.4324 50.7 32.626 50.7 32.8648C50.7 33.1036 50.8934 33.2972 51.1321 33.2972H53.2931C53.5318 33.2972 53.7253 33.1036 53.7253 32.8648C53.7253 32.626 53.5318 32.4324 53.2931 32.4324Z" fill="#F0EDF8"/>
-<path d="M61.9367 29.8378H59.7758C59.5371 29.8378 59.3436 30.0314 59.3436 30.2702C59.3436 30.509 59.5371 30.7026 59.7758 30.7026H61.9367C62.1754 30.7026 62.3689 30.509 62.3689 30.2702C62.3689 30.0314 62.1754 29.8378 61.9367 29.8378Z" fill="#FEE1D3"/>
-<path d="M56.1016 24.6486H55.4533C55.2146 24.6486 55.0211 24.8422 55.0211 25.081C55.0211 25.3198 55.2146 25.5134 55.4533 25.5134H56.1016C56.3403 25.5134 56.5338 25.3198 56.5338 25.081C56.5338 24.8422 56.3403 24.6486 56.1016 24.6486Z" fill="#FC6D26"/>
-<path d="M57.6149 29.8378H56.9666C56.7279 29.8378 56.5344 30.0314 56.5344 30.2702C56.5344 30.509 56.7279 30.7026 56.9666 30.7026H57.6149C57.8536 30.7026 58.0471 30.509 58.0471 30.2702C58.0471 30.0314 57.8536 29.8378 57.6149 29.8378Z" fill="#6B4FBB"/>
-<path d="M56.1016 32.4324H55.4533C55.2146 32.4324 55.0211 32.626 55.0211 32.8648C55.0211 33.1036 55.2146 33.2972 55.4533 33.2972H56.1016C56.3403 33.2972 56.5338 33.1036 56.5338 32.8648C56.5338 32.626 56.3403 32.4324 56.1016 32.4324Z" fill="#FC6D26"/>
-<path d="M51.7804 27.2432H51.1321C50.8934 27.2432 50.7 27.4368 50.7 27.6756C50.7 27.9144 50.8934 28.108 51.1321 28.108H51.7804C52.0191 28.108 52.2126 27.9144 52.2126 27.6756C52.2126 27.4368 52.0191 27.2432 51.7804 27.2432Z" fill="#6B4FBB"/>
-<path d="M57.6153 27.2432H53.9417C53.703 27.2432 53.5095 27.4368 53.5095 27.6756C53.5095 27.9144 53.703 28.108 53.9417 28.108H57.6153C57.854 28.108 58.0475 27.9144 58.0475 27.6756C58.0475 27.4368 57.854 27.2432 57.6153 27.2432Z" fill="#FEE1D3"/>
-<path d="M54.8057 29.8378H51.1321C50.8934 29.8378 50.7 30.0314 50.7 30.2702C50.7 30.509 50.8934 30.7026 51.1321 30.7026H54.8057C55.0444 30.7026 55.2379 30.509 55.2379 30.2702C55.2379 30.0314 55.0444 29.8378 54.8057 29.8378Z" fill="#FEF0E9"/>
-<path d="M58.9112 24.6486H58.2629C58.0242 24.6486 57.8307 24.8422 57.8307 25.081C57.8307 25.3198 58.0242 25.5134 58.2629 25.5134H58.9112C59.1499 25.5134 59.3433 25.3198 59.3433 25.081C59.3433 24.8422 59.1499 24.6486 58.9112 24.6486Z" fill="#6B4FBB"/>
-<path d="M32.1163 35.027H29.9553C29.7166 35.027 29.5231 35.2206 29.5231 35.4594C29.5231 35.6982 29.7166 35.8918 29.9553 35.8918H32.1163C32.355 35.8918 32.5484 35.6982 32.5484 35.4594C32.5484 35.2206 32.355 35.027 32.1163 35.027Z" fill="#F0EDF8"/>
-<path d="M36.4372 35.027H34.2763C34.0376 35.027 33.8441 35.2206 33.8441 35.4594C33.8441 35.6982 34.0376 35.8918 34.2763 35.8918H36.4372C36.6759 35.8918 36.8694 35.6982 36.8694 35.4594C36.8694 35.2206 36.6759 35.027 36.4372 35.027Z" fill="#6B4FBB"/>
-<path d="M40.7597 35.027H38.5988C38.3601 35.027 38.1666 35.2206 38.1666 35.4594C38.1666 35.6982 38.3601 35.8918 38.5988 35.8918H40.7597C40.9984 35.8918 41.1919 35.6982 41.1919 35.4594C41.1919 35.2206 40.9984 35.027 40.7597 35.027Z" fill="#E1DBF1"/>
-<path d="M32.1161 37.6216H29.9551C29.7164 37.6216 29.5229 37.8152 29.5229 38.054C29.5229 38.2928 29.7164 38.4864 29.9551 38.4864H32.1161C32.3548 38.4864 32.5483 38.2928 32.5483 38.054C32.5483 37.8152 32.3548 37.6216 32.1161 37.6216Z" fill="#FEF0E9"/>
-<path d="M40.7597 40.2161H38.5988C38.3601 40.2161 38.1666 40.4097 38.1666 40.6485C38.1666 40.8873 38.3601 41.0809 38.5988 41.0809H40.7597C40.9984 41.0809 41.1919 40.8873 41.1919 40.6485C41.1919 40.4097 40.9984 40.2161 40.7597 40.2161Z" fill="#FEE1D3"/>
-<path d="M34.9246 37.6216H34.2763C34.0376 37.6216 33.8441 37.8152 33.8441 38.054C33.8441 38.2928 34.0376 38.4864 34.2763 38.4864H34.9246C35.1633 38.4864 35.3568 38.2928 35.3568 38.054C35.3568 37.8152 35.1633 37.6216 34.9246 37.6216Z" fill="#EEEEEE"/>
-<path d="M30.6034 40.2161H29.9551C29.7164 40.2161 29.5229 40.4097 29.5229 40.6485C29.5229 40.8873 29.7164 41.0809 29.9551 41.0809H30.6034C30.8421 41.0809 31.0356 40.8873 31.0356 40.6485C31.0356 40.4097 30.8421 40.2161 30.6034 40.2161Z" fill="#6B4FBB"/>
-<path d="M36.4383 40.2161H32.7647C32.526 40.2161 32.3325 40.4097 32.3325 40.6485C32.3325 40.8873 32.526 41.0809 32.7647 41.0809H36.4383C36.677 41.0809 36.8705 40.8873 36.8705 40.6485C36.8705 40.4097 36.677 40.2161 36.4383 40.2161Z" fill="#FEF0E9"/>
-<path d="M37.7342 37.6216H37.0859C36.8472 37.6216 36.6537 37.8152 36.6537 38.054C36.6537 38.2928 36.8472 38.4864 37.0859 38.4864H37.7342C37.9728 38.4864 38.1663 38.2928 38.1663 38.054C38.1663 37.8152 37.9728 37.6216 37.7342 37.6216Z" fill="#FC6D26"/>
-<path d="M46.3791 25.2972C46.3791 25.0584 46.1856 24.8647 45.947 24.8647C45.7083 24.8647 45.5148 25.0584 45.5148 25.2972V38.0539C45.5148 38.2927 45.7083 38.4863 45.947 38.4863C46.1856 38.4863 46.3791 38.2927 46.3791 38.0539V25.2972Z" fill="#EEEEEE"/>
-<path d="M66.9082 15.135H18.0709C17.8322 15.135 17.6387 15.3286 17.6387 15.5674C17.6387 15.8063 17.8322 15.9999 18.0709 15.9999H66.9082C67.1469 15.9999 67.3404 15.8063 67.3404 15.5674C67.3404 15.3286 67.1469 15.135 66.9082 15.135Z" fill="#EEEEEE"/>
-<path d="M55.8884 55.3513C59.5881 55.3513 62.5874 52.3504 62.5874 48.6486C62.5874 44.9468 59.5881 41.9459 55.8884 41.9459C52.1887 41.9459 49.1895 44.9468 49.1895 48.6486C49.1895 52.3504 52.1887 55.3513 55.8884 55.3513Z" fill="white"/>
-<path d="M55.8878 54.9188C59.3489 54.9188 62.1546 52.1115 62.1546 48.6486C62.1546 45.1856 59.3489 42.3783 55.8878 42.3783C52.4268 42.3783 49.6211 45.1856 49.6211 48.6486C49.6211 52.1115 52.4268 54.9188 55.8878 54.9188Z" stroke="#EEEEEE" stroke-width="2"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M55.457 48.2161V46.9188C55.457 46.6799 55.6505 46.4863 55.8892 46.4863C56.1278 46.4863 56.3213 46.6799 56.3213 46.9188V48.2161H57.6179C57.8566 48.2161 58.0501 48.4097 58.0501 48.6485C58.0501 48.8873 57.8566 49.0809 57.6179 49.0809H56.3213V50.3782C56.3213 50.617 56.1278 50.8106 55.8892 50.8106C55.6505 50.8106 55.457 50.617 55.457 50.3782V49.0809H54.1604C53.9217 49.0809 53.7282 48.8873 53.7282 48.6485C53.7282 48.4097 53.9217 48.2161 54.1604 48.2161H55.457Z" fill="#FC6D26"/>
-</svg>
diff --git a/app/assets/images/learn_gitlab/security_scan_enabled.svg b/app/assets/images/learn_gitlab/security_scan_enabled.svg
deleted file mode 100644
index eea0693484c..00000000000
--- a/app/assets/images/learn_gitlab/security_scan_enabled.svg
+++ /dev/null
@@ -1,36 +0,0 @@
-<svg width="47" height="47" viewBox="0 0 47 47" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M23.0588 47C35.7939 47 46.1176 36.6762 46.1176 23.9411C46.1176 11.2061 35.7939 0.882324 23.0588 0.882324C10.3238 0.882324 0 11.2061 0 23.9411C0 36.6762 10.3238 47 23.0588 47Z" fill="#EEEEEE"/>
-<path d="M23.0588 45.1176C34.7542 45.1176 44.2353 35.6366 44.2353 23.9411C44.2353 12.2457 34.7542 2.76465 23.0588 2.76465C11.3634 2.76465 1.88232 12.2457 1.88232 23.9411C1.88232 35.6366 11.3634 45.1176 23.0588 45.1176Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3292 14.5295H16.9802C17.4786 14.5295 17.8827 14.9336 17.8827 15.432V16.8825C17.8827 17.4023 17.4613 17.8237 16.9415 17.8237H13.6556C13.1683 17.8237 12.7732 17.4286 12.7732 16.9413C12.7732 16.6257 12.9419 16.334 13.2155 16.1766L15.8598 14.655C16.0026 14.5728 16.1644 14.5295 16.3292 14.5295ZM12.2356 23.0001H16.9415C17.4613 23.0001 17.8827 23.4215 17.8827 23.9413V25.3531C17.8827 25.8729 17.4613 26.2942 16.9415 26.2942H12.2356C11.7158 26.2942 11.2944 25.8729 11.2944 25.3531V23.9413C11.2944 23.4215 11.7158 23.0001 12.2356 23.0001Z" fill="#EFEDF8"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M6.11793 18.7649H10.8238C11.3436 18.7649 11.765 19.1863 11.765 19.7061V21.1178C11.765 21.6376 11.3436 22.059 10.8238 22.059H6.11793C5.59814 22.059 5.17676 21.6376 5.17676 21.1178V19.7061C5.17676 19.1863 5.59814 18.7649 6.11793 18.7649Z" fill="#F9E2D5"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M40.4709 18.7649H35.765C35.2452 18.7649 34.8239 19.1863 34.8239 19.7061V21.1178C34.8239 21.6376 35.2452 22.059 35.765 22.059H40.4709C40.9907 22.059 41.4121 21.6376 41.4121 21.1178V19.7061C41.4121 19.1863 40.9907 18.7649 40.4709 18.7649Z" fill="#F9E2D5"/>
-<path d="M16.9415 31.0002H12.2356C11.7158 31.0002 11.2944 31.4216 11.2944 31.9414V33.3532C11.2944 33.873 11.7158 34.2944 12.2356 34.2944H16.9415C17.4613 34.2944 17.8827 33.873 17.8827 33.3532V31.9414C17.8827 31.4216 17.4613 31.0002 16.9415 31.0002Z" fill="#F9E2D5"/>
-<path d="M29.6474 31.0002H34.3533C34.8731 31.0002 35.2944 31.4216 35.2944 31.9414V33.3532C35.2944 33.873 34.8731 34.2944 34.3533 34.2944H29.6474C29.1276 34.2944 28.7062 33.873 28.7062 33.3532V31.9414C28.7062 31.4216 29.1276 31.0002 29.6474 31.0002Z" fill="#F9E2D5"/>
-<path d="M9.41202 31.0002H8.00026C7.48046 31.0002 7.05908 31.4216 7.05908 31.9414V33.3532C7.05908 33.873 7.48046 34.2944 8.00026 34.2944H9.41202C9.93182 34.2944 10.3532 33.873 10.3532 33.3532V31.9414C10.3532 31.4216 9.93182 31.0002 9.41202 31.0002Z" fill="#F9E2D5"/>
-<path d="M37.1768 31.0002H38.5886C39.1084 31.0002 39.5298 31.4216 39.5298 31.9414V33.3532C39.5298 33.873 39.1084 34.2944 38.5886 34.2944H37.1768C36.657 34.2944 36.2357 33.873 36.2357 33.3532V31.9414C36.2357 31.4216 36.657 31.0002 37.1768 31.0002Z" fill="#F9E2D5"/>
-<path d="M9.41202 23.0002H8.00026C7.48046 23.0002 7.05908 23.4216 7.05908 23.9414V25.3532C7.05908 25.873 7.48046 26.2944 8.00026 26.2944H9.41202C9.93182 26.2944 10.3532 25.873 10.3532 25.3532V23.9414C10.3532 23.4216 9.93182 23.0002 9.41202 23.0002Z" fill="#F9E2D5"/>
-<path d="M37.1768 23.0002H38.5886C39.1084 23.0002 39.5298 23.4216 39.5298 23.9414V25.3532C39.5298 25.873 39.1084 26.2944 38.5886 26.2944H37.1768C36.657 26.2944 36.2357 25.873 36.2357 25.3532V23.9414C36.2357 23.4216 36.657 23.0002 37.1768 23.0002Z" fill="#F9E2D5"/>
-<path d="M9.41202 14.5295H8.00026C7.48046 14.5295 7.05908 14.9509 7.05908 15.4707V16.8825C7.05908 17.4023 7.48046 17.8237 8.00026 17.8237H9.41202C9.93182 17.8237 10.3532 17.4023 10.3532 16.8825V15.4707C10.3532 14.9509 9.93182 14.5295 9.41202 14.5295Z" fill="#F9E2D5"/>
-<path d="M37.1768 14.5295H38.5886C39.1084 14.5295 39.5298 14.9509 39.5298 15.4707V16.8825C39.5298 17.4023 39.1084 17.8237 38.5886 17.8237H37.1768C36.657 17.8237 36.2357 17.4023 36.2357 16.8825V15.4707C36.2357 14.9509 36.657 14.5295 37.1768 14.5295Z" fill="#F9E2D5"/>
-<path d="M10.8238 26.7649H6.11793C5.59814 26.7649 5.17676 27.1863 5.17676 27.7061V29.1178C5.17676 29.6376 5.59814 30.059 6.11793 30.059H10.8238C11.3436 30.059 11.765 29.6376 11.765 29.1178V27.7061C11.765 27.1863 11.3436 26.7649 10.8238 26.7649Z" fill="#F9E2D5"/>
-<path d="M35.765 26.7649H40.4709C40.9907 26.7649 41.4121 27.1863 41.4121 27.7061V29.1178C41.4121 29.6376 40.9907 30.059 40.4709 30.059H35.765C35.2452 30.059 34.8239 29.6376 34.8239 29.1178V27.7061C34.8239 27.1863 35.2452 26.7649 35.765 26.7649Z" fill="#F9E2D5"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M12.2356 14.5295H16.9415C17.4613 14.5295 17.8827 14.9509 17.8827 15.4707V16.8825C17.8827 17.4023 17.4613 17.8237 16.9415 17.8237H12.2356C11.7158 17.8237 11.2944 17.4023 11.2944 16.8825V15.4707C11.2944 14.9509 11.7158 14.5295 12.2356 14.5295Z" fill="#EFEDF8"/>
-<path d="M28.2354 14.5295H23.5296C23.0098 14.5295 22.5884 14.9509 22.5884 15.4707V16.8825C22.5884 17.4023 23.0098 17.8237 23.5296 17.8237H28.2354C28.7552 17.8237 29.1766 17.4023 29.1766 16.8825V15.4707C29.1766 14.9509 28.7552 14.5295 28.2354 14.5295Z" fill="#EFEDF8"/>
-<path d="M28.2354 23.0002H23.5296C23.0098 23.0002 22.5884 23.4216 22.5884 23.9414V25.3532C22.5884 25.873 23.0098 26.2944 23.5296 26.2944H28.2354C28.7552 26.2944 29.1766 25.873 29.1766 25.3532V23.9414C29.1766 23.4216 28.7552 23.0002 28.2354 23.0002Z" fill="#EFEDF8"/>
-<path d="M32.0002 26.7649H29.6472C29.1274 26.7649 28.7061 27.1863 28.7061 27.7061V29.1178C28.7061 29.6376 29.1274 30.059 29.6472 30.059H32.0002C32.52 30.059 32.9413 29.6376 32.9413 29.1178V27.7061C32.9413 27.1863 32.52 26.7649 32.0002 26.7649Z" fill="#EFEDF8"/>
-<path d="M22.5885 18.7649H17.8826C17.3628 18.7649 16.9414 19.1863 16.9414 19.7061V21.1178C16.9414 21.6376 17.3628 22.059 17.8826 22.059H22.5885C23.1083 22.059 23.5296 21.6376 23.5296 21.1178V19.7061C23.5296 19.1863 23.1083 18.7649 22.5885 18.7649Z" fill="#EFEDF8"/>
-<path d="M21.1767 14.5295H19.7649C19.2451 14.5295 18.8237 14.9509 18.8237 15.4707V16.8825C18.8237 17.4023 19.2451 17.8237 19.7649 17.8237H21.1767C21.6965 17.8237 22.1178 17.4023 22.1178 16.8825V15.4707C22.1178 14.9509 21.6965 14.5295 21.1767 14.5295Z" fill="#EFEDF8"/>
-<path d="M21.1767 23.0002H19.7649C19.2451 23.0002 18.8237 23.4216 18.8237 23.9414V25.3532C18.8237 25.873 19.2451 26.2944 19.7649 26.2944H21.1767C21.6965 26.2944 22.1178 25.873 22.1178 25.3532V23.9414C22.1178 23.4216 21.6965 23.0002 21.1767 23.0002Z" fill="#EFEDF8"/>
-<path d="M15.059 26.7649H13.6472C13.1274 26.7649 12.7061 27.1863 12.7061 27.7061V29.1178C12.7061 29.6376 13.1274 30.059 13.6472 30.059H15.059C15.5788 30.059 16.0002 29.6376 16.0002 29.1178V27.7061C16.0002 27.1863 15.5788 26.7649 15.059 26.7649Z" fill="#EFEDF8"/>
-<path d="M21.1767 31.0002H19.7649C19.2451 31.0002 18.8237 31.4216 18.8237 31.9414V33.3532C18.8237 33.873 19.2451 34.2944 19.7649 34.2944H21.1767C21.6965 34.2944 22.1178 33.873 22.1178 33.3532V31.9414C22.1178 31.4216 21.6965 31.0002 21.1767 31.0002Z" fill="#EFEDF8"/>
-<path d="M32.4706 23.0002H31.0589C30.5391 23.0002 30.1177 23.4216 30.1177 23.9414V25.3532C30.1177 25.873 30.5391 26.2944 31.0589 26.2944H32.4706C32.9904 26.2944 33.4118 25.873 33.4118 25.3532V23.9414C33.4118 23.4216 32.9904 23.0002 32.4706 23.0002Z" fill="#EFEDF8"/>
-<path d="M15.059 18.7649H13.6472C13.1274 18.7649 12.7061 19.1863 12.7061 19.7061V21.1178C12.7061 21.6376 13.1274 22.059 13.6472 22.059H15.059C15.5788 22.059 16.0002 21.6376 16.0002 21.1178V19.7061C16.0002 19.1863 15.5788 18.7649 15.059 18.7649Z" fill="#EFEDF8"/>
-<path d="M26.8241 18.7649H25.4124C24.8926 18.7649 24.4712 19.1863 24.4712 19.7061V21.1178C24.4712 21.6376 24.8926 22.059 25.4124 22.059H26.8241C27.3439 22.059 27.7653 21.6376 27.7653 21.1178V19.7061C27.7653 19.1863 27.3439 18.7649 26.8241 18.7649Z" fill="#EFEDF8"/>
-<path d="M32.4706 14.5295H31.0589C30.5391 14.5295 30.1177 14.9509 30.1177 15.4707V16.8825C30.1177 17.4023 30.5391 17.8237 31.0589 17.8237H32.4706C32.9904 17.8237 33.4118 17.4023 33.4118 16.8825V15.4707C33.4118 14.9509 32.9904 14.5295 32.4706 14.5295Z" fill="#EFEDF8"/>
-<path d="M34.3531 18.7649H29.6472C29.1274 18.7649 28.7061 19.1863 28.7061 19.7061V21.1178C28.7061 21.6376 29.1274 22.059 29.6472 22.059H34.3531C34.8729 22.059 35.2943 21.6376 35.2943 21.1178V19.7061C35.2943 19.1863 34.8729 18.7649 34.3531 18.7649Z" fill="#EFEDF8"/>
-<path d="M22.5885 26.7649H17.8826C17.3628 26.7649 16.9414 27.1863 16.9414 27.7061V29.1178C16.9414 29.6376 17.3628 30.059 17.8826 30.059H22.5885C23.1083 30.059 23.5296 29.6376 23.5296 29.1178V27.7061C23.5296 27.1863 23.1083 26.7649 22.5885 26.7649Z" fill="#EFEDF8"/>
-<path d="M28.2354 31.0002H23.5296C23.0098 31.0002 22.5884 31.4216 22.5884 31.9414V33.3532C22.5884 33.873 23.0098 34.2944 23.5296 34.2944H28.2354C28.7552 34.2944 29.1766 33.873 29.1766 33.3532V31.9414C29.1766 31.4216 28.7552 31.0002 28.2354 31.0002Z" fill="#EFEDF8"/>
-<path d="M26.8241 26.7649H25.4124C24.8926 26.7649 24.4712 27.1863 24.4712 27.7061V29.1178C24.4712 29.6376 24.8926 30.059 25.4124 30.059H26.8241C27.3439 30.059 27.7653 29.6376 27.7653 29.1178V27.7061C27.7653 27.1863 27.3439 26.7649 26.8241 26.7649Z" fill="#EFEDF8"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M23.5516 10.3323L36.0222 13.8791C36.4267 13.9942 36.7059 14.3638 36.7059 14.7844V21.1043C36.7059 25.151 35.0909 29.032 32.2166 31.8933L23.9581 40.1143C23.5909 40.4798 22.9973 40.4798 22.6301 40.1143L14.3717 31.8933C11.4972 29.032 9.88232 25.151 9.88232 21.1043V14.7844C9.88232 14.3638 10.1614 13.9942 10.566 13.8791L23.0366 10.3323C23.2049 10.2844 23.3833 10.2844 23.5516 10.3323ZM23.1301 12.4046L11.9603 15.5579C11.7575 15.6151 11.6175 15.8001 11.6175 16.0108V20.6639C11.6175 24.3243 13.0892 27.8348 15.7088 30.4231L22.9272 37.5553C23.1105 37.7364 23.4054 37.7364 23.5887 37.5553L30.807 30.4231C33.4268 27.8348 34.8983 24.3243 34.8983 20.6639V16.0108C34.8983 15.8001 34.7583 15.6151 34.5556 15.5579L23.3858 12.4046C23.3022 12.381 23.2137 12.381 23.1301 12.4046Z" fill="white"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M23.3165 11.7436L34.14 14.8206C34.5447 14.9356 34.8238 15.3052 34.8238 15.7259V21.1793C34.8238 24.7274 33.4072 28.1301 30.8859 30.6389L23.723 37.7659C23.3558 38.1312 22.7625 38.1312 22.3953 37.7659L15.2325 30.6389C12.711 28.1301 11.2944 24.7274 11.2944 21.1793V15.7259C11.2944 15.3052 11.5736 14.9356 11.9782 14.8206L22.8018 11.7436C22.97 11.6958 23.1483 11.6958 23.3165 11.7436ZM22.8996 13.5561L13.1593 16.3045C12.9566 16.3617 12.8166 16.5467 12.8166 16.7574V20.7932C12.8166 24.0026 14.1075 27.0805 16.4054 29.3498L22.6968 35.5631C22.88 35.7442 23.1748 35.7442 23.3581 35.5631L29.6494 29.3498C31.9474 27.0805 33.2383 24.0026 33.2383 20.7932V16.7574C33.2383 16.5467 33.0983 16.3617 32.8955 16.3045L23.1552 13.5561C23.0717 13.5325 22.9832 13.5325 22.8996 13.5561Z" fill="#6E49CB"/>
-</svg>
diff --git a/app/assets/images/learn_gitlab/trial_started.svg b/app/assets/images/learn_gitlab/trial_started.svg
deleted file mode 100644
index 42d6fb6c013..00000000000
--- a/app/assets/images/learn_gitlab/trial_started.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-<svg width="32" height="30" viewBox="0 0 32 30" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9653 29.8263L21.8368 11.6285H10.0933L15.9653 29.8263Z" fill="#E38800"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9656 29.8263L10.0936 11.6285H1.86475L15.9656 29.8263Z" fill="#F7980A"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M1.86441 11.6285L0.0800968 17.1586C-0.0826524 17.663 0.0955967 18.2156 0.521693 18.5273L15.9652 29.8261L1.86441 11.6285Z" fill="#FCA326"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M1.86426 11.6286H10.0933L6.55678 0.668335C6.37489 0.104294 5.58257 0.104447 5.40067 0.668335L1.86426 11.6286Z" fill="#E38800"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9653 29.8263L21.8369 11.6285H30.0658L15.9653 29.8263Z" fill="#F7980A"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M30.0662 11.6285L31.8505 17.1586C32.0132 17.663 31.835 18.2156 31.4089 18.5273L15.9653 29.8261L30.0662 11.6285Z" fill="#FCA326"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M30.066 11.6286H21.8369L25.3735 0.668335C25.5554 0.104294 26.3477 0.104447 26.5296 0.668335L30.066 11.6286Z" fill="#E38800"/>
-</svg>
diff --git a/app/assets/images/learn_gitlab/user_added.svg b/app/assets/images/learn_gitlab/user_added.svg
deleted file mode 100644
index efbccff0bbb..00000000000
--- a/app/assets/images/learn_gitlab/user_added.svg
+++ /dev/null
@@ -1,4 +0,0 @@
-<svg width="38" height="24" viewBox="0 0 38 24" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M33.6353 7.51765H33.8824L32.4706 5.80941L31.0588 7.50353L29.6471 5.80941L28.2353 7.50353L26.8235 5.80941L25.4118 7.50353L24 5.80941L22.5882 7.51765H23.1318C23.6965 8.89412 24 10.4118 24 12C24 14.0047 23.5059 15.8824 22.6447 17.5482C24.0353 19.1576 26.0541 20.1176 28.2353 20.1176C32.3294 20.1176 35.6471 16.7718 35.6471 12.6353C35.6471 10.6588 34.8847 8.85177 33.6353 7.51765ZM22.0094 5.36471C23.7035 3.88235 25.9059 3.03529 28.2353 3.03529C33.5012 3.03529 37.7647 7.34118 37.7647 12.6353C37.7647 17.9294 33.5012 22.2353 28.2353 22.2353C25.6376 22.2353 23.2235 21.1765 21.4588 19.3835C19.2706 22.1929 15.84 24 12 24C5.36471 24 0 18.6353 0 12C0 5.36471 5.36471 0 12 0C14.2729 0 16.3976 0.635295 18.2118 1.72941C19.7153 2.64706 21.0141 3.88235 22.0094 5.37177V5.36471ZM3.52941 8.47059C3.07059 9.55765 2.82353 10.7506 2.82353 12C2.82353 17.0682 6.93177 21.1765 12 21.1765C17.0682 21.1765 21.1765 17.0682 21.1765 12C21.1765 10.7506 20.9294 9.55765 20.4706 8.47059H14.1176C13.7435 8.47059 13.3835 8.32941 13.1294 8.04706L12 6.94588L10.8706 8.06118C10.6165 8.34353 10.2565 8.48471 9.88235 8.48471H3.52941V8.47059ZM18.6212 5.64706C16.9271 3.88235 14.5553 2.82353 12 2.82353C9.44471 2.82353 7.07294 3.88235 5.37882 5.64706H9.29647L11.0047 3.95294C11.5271 3.38824 12.4165 3.38824 12.9812 3.95294L14.6753 5.64706H18.5859H18.6212Z" fill="#E1DBF2"/>
-<path fill-rule="evenodd" clip-rule="evenodd" d="M25.1292 14.3435C24.6633 14.3435 24.2821 13.9623 24.2821 13.4964C24.2821 13.0164 24.6633 12.6353 25.1292 12.6353C25.5951 12.6353 25.9762 13.0164 25.9762 13.4823C25.9762 13.9623 25.5951 14.3435 25.1292 14.3435ZM31.3409 14.3435C30.8751 14.3435 30.4939 13.9623 30.4939 13.4964C30.4939 13.0164 30.8751 12.6353 31.3409 12.6353C31.8068 12.6353 32.188 13.0164 32.188 13.4823C32.188 13.9623 31.8068 14.3435 31.3409 14.3435ZM9.17624 15.5294H14.8233C14.8233 17.0823 13.5527 18.3529 11.9998 18.3529C10.4468 18.3529 9.17624 17.0823 9.17624 15.5294ZM8.11742 14.8235C7.53153 14.8235 7.05859 14.3505 7.05859 13.7647C7.05859 13.1788 7.53153 12.7058 8.11742 12.7058C8.7033 12.7058 9.17624 13.1788 9.17624 13.7647C9.17624 14.3505 8.7033 14.8235 8.11742 14.8235ZM15.8821 14.8235C15.2962 14.8235 14.8233 14.3505 14.8233 13.7647C14.8233 13.1788 15.2962 12.7058 15.8821 12.7058C16.468 12.7058 16.9409 13.1788 16.9409 13.7647C16.9409 14.3505 16.468 14.8235 15.8821 14.8235Z" fill="#6B4FBB"/>
-</svg>
diff --git a/app/assets/javascripts/alert_management/list.js b/app/assets/javascripts/alert_management/list.js
index e9d19f18ab5..57d1f135606 100644
--- a/app/assets/javascripts/alert_management/list.js
+++ b/app/assets/javascripts/alert_management/list.js
@@ -39,6 +39,7 @@ export default () => {
return defaultDataIdFromObject(object);
},
},
+ assumeImmutableResults: true,
},
),
});
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
index 84189b675f2..52901d4c5bb 100644
--- a/app/assets/javascripts/analytics/shared/utils.js
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -1,4 +1,9 @@
+import dateFormat from 'dateformat';
+import { dateFormats } from './constants';
+
export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
if (!searchTerm?.length) return data;
return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
};
+
+export const toYmd = (date) => dateFormat(date, dateFormats.isoDate);
diff --git a/app/assets/javascripts/analytics/usage_trends/index.js b/app/assets/javascripts/analytics/usage_trends/index.js
index d1880b09f15..3e85832edcf 100644
--- a/app/assets/javascripts/analytics/usage_trends/index.js
+++ b/app/assets/javascripts/analytics/usage_trends/index.js
@@ -6,7 +6,7 @@ import UsageTrendsApp from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export default () => {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 84a5d5ae4b3..01e463c1965 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -870,6 +870,14 @@ const Api = {
return axios.put(url, freezePeriod);
},
+ deleteFreezePeriod(id, freezePeriodId) {
+ const url = Api.buildUrl(this.freezePeriodPath)
+ .replace(':id', encodeURIComponent(id))
+ .replace(':freeze_period_id', encodeURIComponent(freezePeriodId));
+
+ return axios.delete(url);
+ },
+
trackRedisCounterEvent(event) {
if (!gon.features?.usageDataApi) {
return null;
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index 1cd7fb0b954..b018db9a02d 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -3,6 +3,7 @@ import axios from '../lib/utils/axios_utils';
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';
export function getProjects(query, options, callback = () => {}) {
const url = buildApiUrl(PROJECTS_PATH);
@@ -25,3 +26,10 @@ export function getProjects(query, options, callback = () => {}) {
return { data, headers };
});
}
+
+export function importProjectMembers(sourceId, targetId) {
+ const url = buildApiUrl(PROJECT_IMPORT_MEMBERS_PATH)
+ .replace(':id', sourceId)
+ .replace(':project_id', targetId);
+ return axios.post(url);
+}
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
index f89600fbed3..fe801cd460f 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -165,7 +165,7 @@ export default {
:title="$options.i18n.proceedButton"
variant="confirm"
data-qa-selector="proceed_button"
- data-track-event="click_button"
+ data-track-action="click_button"
:data-track-label="`${$options.trackingLabelPrefix}proceed_button`"
>{{ $options.i18n.proceedButton }}</gl-button
>
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 0a05e0d44ce..8381dcec9c3 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -6,7 +6,7 @@ export default class Autosave {
constructor(field, key, fallbackKey, lockVersion) {
this.field = field;
- this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
if (key.join != null) {
key = key.join('/');
}
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 7e605099655..2c7e878f044 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
+import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
import createFlash from '~/flash';
@@ -19,6 +18,9 @@ export default {
GlFormInput,
GlFormGroup,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
isEditing: {
type: Boolean,
@@ -168,6 +170,7 @@ export default {
});
},
},
+ safeHtmlConfig: { ALLOW_TAGS: ['a', 'code'] },
};
</script>
@@ -184,7 +187,7 @@ export default {
<div class="form-group">
<label for="badge-link-url" class="label-bold">{{ s__('Badges|Link') }}</label>
- <p v-html="helpText"></p>
+ <p v-safe-html:[$options.safeHtmlConfig]="helpText"></p>
<input
id="badge-link-url"
v-model="linkUrl"
@@ -199,7 +202,7 @@ export default {
<div class="form-group">
<label for="badge-image-url" class="label-bold">{{ s__('Badges|Badge image URL') }}</label>
- <p v-html="helpText"></p>
+ <p v-safe-html:[$options.safeHtmlConfig]="helpText"></p>
<input
id="badge-image-url"
v-model="imageUrl"
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index 96c3b8276ee..f5e3bab6ff0 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -106,7 +105,7 @@ export default {
<div
v-if="draftCommands"
class="referenced-commands draft-note-commands"
- v-html="draftCommands"
+ v-html="draftCommands /* eslint-disable-line vue/no-v-html */"
></div>
<p class="draft-note-actions d-flex">
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index 080a5543e53..bce13751448 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import { REVIEW_BAR_VISIBLE_CLASS_NAME } from '../constants';
import PreviewDropdown from './preview_dropdown.vue';
import PublishButton from './publish_button.vue';
@@ -10,7 +11,6 @@ export default {
},
computed: {
...mapGetters(['isNotesFetched']),
- ...mapGetters('batchComments', ['draftsCount']),
},
watch: {
isNotesFetched() {
@@ -19,13 +19,19 @@ export default {
}
},
},
+ mounted() {
+ document.body.classList.add(REVIEW_BAR_VISIBLE_CLASS_NAME);
+ },
+ beforeDestroy() {
+ document.body.classList.remove(REVIEW_BAR_VISIBLE_CLASS_NAME);
+ },
methods: {
...mapActions('batchComments', ['expandAllDiscussions']),
},
};
</script>
<template>
- <div v-show="draftsCount > 0">
+ <div>
<nav class="review-bar-component" data-testid="review_bar_component">
<div
class="review-bar-content d-flex gl-justify-content-end"
diff --git a/app/assets/javascripts/batch_comments/constants.js b/app/assets/javascripts/batch_comments/constants.js
index b309c339fc8..5e026251e0b 100644
--- a/app/assets/javascripts/batch_comments/constants.js
+++ b/app/assets/javascripts/batch_comments/constants.js
@@ -1,3 +1,5 @@
export const CHANGES_TAB = 'diffs';
export const DISCUSSION_TAB = 'notes';
export const SHOW_TAB = 'show';
+
+export const REVIEW_BAR_VISIBLE_CLASS_NAME = 'review-bar-visible';
diff --git a/app/assets/javascripts/batch_comments/index.js b/app/assets/javascripts/batch_comments/index.js
index 9c763e70d63..65fd34dcb00 100644
--- a/app/assets/javascripts/batch_comments/index.js
+++ b/app/assets/javascripts/batch_comments/index.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
-import { mapActions } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import store from '~/mr_notes/stores';
-import ReviewBar from './components/review_bar.vue';
export const initReviewBar = () => {
const el = document.getElementById('js-review-bar');
@@ -10,6 +9,12 @@ export const initReviewBar = () => {
new Vue({
el,
store,
+ components: {
+ ReviewBar: () => import('./components/review_bar.vue'),
+ },
+ computed: {
+ ...mapGetters('batchComments', ['draftsCount']),
+ },
mounted() {
this.fetchDrafts();
},
@@ -17,7 +22,9 @@ export const initReviewBar = () => {
...mapActions('batchComments', ['fetchDrafts']),
},
render(createElement) {
- return createElement(ReviewBar);
+ if (this.draftsCount === 0) return null;
+
+ return createElement('review-bar');
},
});
};
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/playable.js b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
index 33bb6e0c31c..2b667aba2d6 100644
--- a/app/assets/javascripts/behaviors/markdown/nodes/playable.js
+++ b/app/assets/javascripts/behaviors/markdown/nodes/playable.js
@@ -3,7 +3,6 @@
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
import { Node } from 'tiptap';
-import { HIGHER_PARSE_RULE_PRIORITY } from '../constants';
/**
* Abstract base class for playable media, like video and audio.
@@ -33,33 +32,33 @@ export default class Playable extends Node {
const parseDOM = [
{
tag: `.${this.mediaType}-container`,
- skip: true,
- },
- {
- tag: `.${this.mediaType}-container p`,
- priority: HIGHER_PARSE_RULE_PRIORITY,
- ignore: true,
- },
- {
- tag: `${this.mediaType}[src]`,
- getAttrs: (el) => ({ src: el.src, alt: el.dataset.title }),
+ getAttrs: (el) => ({
+ src: el.querySelector(this.mediaType).src,
+ alt: el.querySelector(this.mediaType).dataset.title,
+ }),
},
];
const toDOM = (node) => [
- this.mediaType,
- {
- src: node.attrs.src,
- controls: true,
- 'data-setup': '{}',
- 'data-title': node.attrs.alt,
- ...this.extraElementAttrs,
- },
+ 'span',
+ { class: `media-container ${this.mediaType}-container` },
+ [
+ this.mediaType,
+ {
+ src: node.attrs.src,
+ controls: true,
+ 'data-setup': '{}',
+ 'data-title': node.attrs.alt,
+ ...this.extraElementAttrs,
+ },
+ ],
+ ['a', { href: node.attrs.src }, node.attrs.alt],
];
return {
attrs,
- group: 'block',
+ group: 'inline',
+ inline: true,
draggable: true,
parseDOM,
toDOM,
@@ -68,6 +67,5 @@ export default class Playable extends Node {
toMarkdown(state, node) {
defaultMarkdownSerializer.nodes.image(state, node);
- state.closeBlock(node);
}
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 005ef103ded..ebf2ab0381e 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -19,7 +19,7 @@ export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations';
*/
export const getCustomizations = memoize(() => {
let parsedCustomizations = {};
- const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
+ const localStorageIsSafe = AccessorUtilities.canUseLocalStorage();
if (localStorageIsSafe) {
try {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
index 8f1518a1c9c..cf7a71d4206 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_toggle.vue
@@ -13,7 +13,7 @@ export default {
},
data() {
return {
- localStorageUsable: AccessorUtilities.isLocalStorageAccessSafe(),
+ localStorageUsable: AccessorUtilities.canUseLocalStorage(),
shortcutsEnabled: !shouldDisableShortcuts(),
};
},
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 470c679b8ba..387d6043315 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -31,7 +31,6 @@ export default class BlobFileDropzone {
autoProcessQueue: false,
url: form.attr('action'),
// Rails uses a hidden input field for PUT
- // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
method,
clickable: true,
uploadMultiple: false,
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
index a8c94b6263e..25fe29c4fbe 100644
--- a/app/assets/javascripts/blob/notebook/index.js
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -6,6 +6,9 @@ export default () => {
return new Vue({
el,
+ provide: {
+ relativeRawPath: el.dataset.relativeRawPath,
+ },
render(createElement) {
return createElement(NotebookViewer, {
props: {
diff --git a/app/assets/javascripts/blob/notebook/notebook_viewer.vue b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
index 02f93e14219..d2a841c88f1 100644
--- a/app/assets/javascripts/blob/notebook/notebook_viewer.vue
+++ b/app/assets/javascripts/blob/notebook/notebook_viewer.vue
@@ -77,3 +77,9 @@ export default {
</p>
</div>
</template>
+
+<style>
+.output img {
+ min-width: 0; /* https://www.w3.org/TR/css-flexbox-1/#min-size-auto */
+}
+</style>
diff --git a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
index fdaa4b082f7..a3278f8bde2 100644
--- a/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
+++ b/app/assets/javascripts/blob/pipeline_tour_success_modal.vue
@@ -124,7 +124,7 @@ export default {
:href="goToMergeRequestPath"
:data-track-property="humanAccess"
:data-track-value="$options.goToTrackValueMergeRequest"
- :data-track-event="$options.trackEvent"
+ :data-track-action="$options.trackEvent"
:data-track-label="trackLabel"
>
{{ $options.i18n.mergeRequestButton }}
@@ -135,7 +135,7 @@ export default {
variant="success"
:data-track-property="humanAccess"
:data-track-value="$options.goToTrackValuePipelines"
- :data-track-event="$options.trackEvent"
+ :data-track-action="$options.trackEvent"
:data-track-label="trackLabel"
>
{{ $options.i18n.pipelinesButton }}
diff --git a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
index aee8bf15e44..e0b0857f7b4 100644
--- a/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
+++ b/app/assets/javascripts/blob/suggest_gitlab_ci_yml/components/popover.vue
@@ -121,7 +121,7 @@ export default {
icon="close"
:data-track-property="humanAccess"
:data-track-value="$options.dismissTrackValue"
- :data-track-event="$options.clickTrackValue"
+ :data-track-action="$options.clickTrackValue"
:data-track-label="trackLabel"
@click="onDismiss"
/>
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index 3219d74f85f..d113a1d39d8 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -1,6 +1,5 @@
import { sortBy, cloneDeep } from 'lodash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ListType } from './constants';
+import { ListType, MilestoneIDs } from './constants';
export function getMilestone() {
return null;
@@ -49,12 +48,10 @@ export function formatListIssues(listIssues) {
return {
...map,
[list.id]: sortedIssues.map((i) => {
- const id = getIdFromGraphQLId(i.id);
+ const { id } = i;
const listIssue = {
...i,
- id,
- fullId: i.id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
};
@@ -108,7 +105,10 @@ export function formatIssueInput(issueInput, boardConfig) {
return {
...issueInput,
- milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null,
+ milestoneId:
+ milestoneId && milestoneId !== MilestoneIDs.ANY
+ ? fullMilestoneId(milestoneId)
+ : issueInput?.milestoneId,
labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])],
assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])],
};
diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue
index d4b559add6e..22ad619e76b 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column.vue
@@ -2,9 +2,6 @@
import { GlFormRadio, GlFormRadioGroup, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
-import { ListType } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
export default {
components: {
@@ -24,7 +21,7 @@ export default {
},
computed: {
...mapState(['labels', 'labelsLoading']),
- ...mapGetters(['getListByLabelId', 'shouldUseGraphQL']),
+ ...mapGetters(['getListByLabelId']),
columnForSelected() {
return this.getListByLabelId(this.selectedId);
},
@@ -34,17 +31,6 @@ export default {
},
methods: {
...mapActions(['createList', 'fetchLabels', 'highlightList', 'setAddColumnFormVisibility']),
- highlight(listId) {
- if (this.shouldUseGraphQL) {
- this.highlightList(listId);
- } else {
- const list = boardsStore.state.lists.find(({ id }) => id === listId);
- list.highlighted = true;
- setTimeout(() => {
- list.highlighted = false;
- }, 2000);
- }
- },
addList() {
if (!this.selectedLabel) {
return;
@@ -54,23 +40,11 @@ export default {
if (this.columnForSelected) {
const listId = this.columnForSelected.id;
- this.highlight(listId);
+ this.highlightList(listId);
return;
}
- if (this.shouldUseGraphQL) {
- this.createList({ labelId: this.selectedId });
- } else {
- const listObj = {
- labelId: getIdFromGraphQLId(this.selectedId),
- title: this.selectedLabel.title,
- position: boardsStore.state.lists.length - 2,
- list_type: ListType.label,
- label: this.selectedLabel,
- };
-
- boardsStore.new(listObj);
- }
+ this.createList({ labelId: this.selectedId });
},
filterItems(searchTerm) {
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
new file mode 100644
index 00000000000..28f4a267077
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -0,0 +1,29 @@
+<script>
+import { mapActions, mapGetters } from 'vuex';
+import BoardContent from '~/boards/components/board_content.vue';
+import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
+
+export default {
+ components: {
+ BoardContent,
+ BoardSettingsSidebar,
+ },
+ inject: ['disabled'],
+ computed: {
+ ...mapGetters(['isSidebarOpen']),
+ },
+ mounted() {
+ this.performSearch();
+ },
+ methods: {
+ ...mapActions(['performSearch']),
+ },
+};
+</script>
+
+<template>
+ <div class="boards-app gl-relative" :class="{ 'is-compact': isSidebarOpen }">
+ <board-content :disabled="disabled" />
+ <board-settings-sidebar />
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_card_deprecated.vue b/app/assets/javascripts/boards/components/board_card_deprecated.vue
deleted file mode 100644
index e12a2836f67..00000000000
--- a/app/assets/javascripts/boards/components/board_card_deprecated.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-// This component is being replaced in favor of './board_card.vue' for GraphQL boards
-import sidebarEventHub from '~/sidebar/event_hub';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
-import BoardCardLayoutDeprecated from './board_card_layout_deprecated.vue';
-
-export default {
- components: {
- BoardCardLayout: BoardCardLayoutDeprecated,
- },
- props: {
- list: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- issue: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
- methods: {
- // These are methods instead of computed's, because boardsStore is not reactive.
- isActive() {
- return this.getActiveId() === this.issue.id;
- },
- getActiveId() {
- return boardsStore.detail?.issue?.id;
- },
- showIssue({ isMultiSelect }) {
- // If no issues are opened, close all sidebars first
- if (!this.getActiveId()) {
- sidebarEventHub.$emit('sidebar.closeAll');
- }
- if (this.isActive()) {
- eventHub.$emit('clearDetailIssue', isMultiSelect);
-
- if (isMultiSelect) {
- eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- }
- } else {
- eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
- boardsStore.setListDetail(this.list);
- }
- },
- },
-};
-</script>
-
-<template>
- <board-card-layout
- data-qa-selector="board_card"
- :issue="issue"
- :list="list"
- :is-active="isActive()"
- v-bind="$attrs"
- @show="showIssue"
- />
-</template>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 5658a34e9a6..db80d48239b 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -214,10 +214,19 @@ export default {
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
+ <gl-icon
+ v-if="item.hidden"
+ v-gl-tooltip
+ name="spam"
+ :title="__('This issue is hidden because its author has been banned')"
+ class="gl-mr-2 hidden-icon"
+ data-testid="hidden-icon"
+ />
<a
:href="item.path || item.webUrl || ''"
:title="item.title"
:class="{ 'gl-text-gray-400!': item.isLoading }"
+ class="js-no-trigger"
@mousemove.stop
>{{ item.title }}</a
>
diff --git a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue b/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
deleted file mode 100644
index 3381e4c3a7d..00000000000
--- a/app/assets/javascripts/boards/components/board_card_layout_deprecated.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { mapActions, mapGetters } from 'vuex';
-import { ISSUABLE } from '~/boards/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import boardsStore from '../stores/boards_store';
-import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
-
-export default {
- name: 'BoardCardLayout',
- components: {
- IssueCardInner: IssueCardInnerDeprecated,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- list: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- issue: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- disabled: {
- type: Boolean,
- default: false,
- required: false,
- },
- index: {
- type: Number,
- default: 0,
- required: false,
- },
- isActive: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- showDetail: false,
- multiSelect: boardsStore.multiSelect,
- };
- },
- computed: {
- ...mapGetters(['isSwimlanesOn']),
- multiSelectVisible() {
- return this.multiSelect.list.findIndex((issue) => issue.id === this.issue.id) > -1;
- },
- },
- methods: {
- ...mapActions(['setActiveId']),
- mouseDown() {
- this.showDetail = true;
- },
- mouseMove() {
- this.showDetail = false;
- },
- showIssue(e) {
- // Don't do anything if this happened on a no trigger element
- if (e.target.classList.contains('js-no-trigger')) return;
-
- if (this.glFeatures.graphqlBoardLists || this.isSwimlanesOn) {
- this.setActiveId({ id: this.issue.id, sidebarType: ISSUABLE });
- return;
- }
-
- const isMultiSelect = e.ctrlKey || e.metaKey;
-
- if (this.showDetail || isMultiSelect) {
- this.showDetail = false;
- this.$emit('show', { event: e, isMultiSelect });
- }
- },
- },
-};
-</script>
-
-<template>
- <li
- :class="{
- 'multi-select': multiSelectVisible,
- 'user-can-drag': !disabled && issue.id,
- 'is-disabled': disabled || !issue.id,
- 'is-active': isActive,
- }"
- :index="index"
- :data-issue-id="issue.id"
- :data-issue-iid="issue.iid"
- :data-issue-path="issue.referencePath"
- data-testid="board_card"
- class="board-card gl-p-5 gl-rounded-base"
- @mousedown="mouseDown"
- @mousemove="mouseMove"
- @mouseup="showIssue($event)"
- >
- <issue-card-inner :list="list" :issue="issue" :update-filters="true" />
- </li>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_column_deprecated.vue b/app/assets/javascripts/boards/components/board_column_deprecated.vue
deleted file mode 100644
index 7c090dfaa53..00000000000
--- a/app/assets/javascripts/boards/components/board_column_deprecated.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
-// This component is being replaced in favor of './board_column.vue' for GraphQL boards
-import Sortable from 'sortablejs';
-import BoardListHeader from 'ee_else_ce/boards/components/board_list_header_deprecated.vue';
-import { getBoardSortableDefaultOptions, sortableEnd } from '../mixins/sortable_default_options';
-import boardsStore from '../stores/boards_store';
-import BoardList from './board_list_deprecated.vue';
-
-export default {
- components: {
- BoardListHeader,
- BoardList,
- },
- inject: {
- boardId: {
- default: '',
- },
- },
- props: {
- list: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- disabled: {
- type: Boolean,
- required: true,
- },
- },
- data() {
- return {
- detailIssue: boardsStore.detail,
- filter: boardsStore.filter,
- };
- },
- computed: {
- listIssues() {
- return this.list.issues;
- },
- },
- watch: {
- filter: {
- handler() {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.page = 1;
- this.list.getIssues(true).catch(() => {
- // TODO: handle request error
- });
- },
- deep: true,
- },
- 'list.highlighted': {
- handler(highlighted) {
- if (highlighted) {
- this.$nextTick(() => {
- this.$el.scrollIntoView({ behavior: 'smooth', block: 'start' });
- });
- }
- },
- immediate: true,
- },
- },
- mounted() {
- const instance = this;
-
- const sortableOptions = getBoardSortableDefaultOptions({
- disabled: this.disabled,
- group: 'boards',
- draggable: '.is-draggable',
- handle: '.js-board-handle',
- onEnd(e) {
- sortableEnd();
-
- const sortable = this;
-
- if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
- const order = sortable.toArray();
- const list = boardsStore.findList('id', parseInt(e.item.dataset.id, 10));
-
- instance.$nextTick(() => {
- boardsStore.moveList(list, order);
- });
- }
- },
- });
-
- Sortable.create(this.$el.parentNode, sortableOptions);
- },
-};
-</script>
-
-<template>
- <div
- :class="{
- 'is-draggable': !list.preset,
- 'is-expandable': list.isExpandable,
- 'is-collapsed': !list.isExpanded,
- 'board-type-assignee': list.type === 'assignee',
- }"
- :data-id="list.id"
- class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
- data-qa-selector="board_list"
- >
- <div
- class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
- :class="{ 'board-column-highlighted': list.highlighted }"
- >
- <board-list-header :list="list" :disabled="disabled" />
- <board-list ref="board-list" :disabled="disabled" :issues="listIssues" :list="list" />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 4df6ff75249..27ea2e7a608 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -5,31 +5,22 @@ import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import defaultSortableConfig from '~/sortable/sortable_config';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DraggableItemTypes } from '../constants';
import BoardColumn from './board_column.vue';
-import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
draggableItemTypes: DraggableItemTypes,
components: {
BoardAddNewColumn,
BoardColumn,
- BoardColumnDeprecated,
BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'),
EpicBoardContentSidebar: () =>
import('ee_component/boards/components/epic_board_content_sidebar.vue'),
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
- mixins: [glFeatureFlagMixin()],
inject: ['canAdminList'],
props: {
- lists: {
- type: Array,
- required: false,
- default: () => [],
- },
disabled: {
type: Boolean,
required: true,
@@ -37,20 +28,15 @@ export default {
},
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
- ...mapGetters(['isSwimlanesOn', 'isEpicBoard']),
- useNewBoardColumnComponent() {
- return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard;
- },
+ ...mapGetters(['isSwimlanesOn', 'isEpicBoard', 'isIssueBoard']),
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
boardListsToUse() {
- return this.useNewBoardColumnComponent
- ? sortBy([...Object.values(this.boardLists)], 'position')
- : this.lists;
+ return sortBy([...Object.values(this.boardLists)], 'position');
},
canDragColumns() {
- return (this.isEpicBoard || this.glFeatures.graphqlBoardLists) && this.canAdminList;
+ return this.canAdminList;
},
boardColumnWrapper() {
return this.canDragColumns ? Draggable : 'div';
@@ -68,9 +54,6 @@ export default {
return this.canDragColumns ? options : {};
},
- boardColumnComponent() {
- return this.useNewBoardColumnComponent ? BoardColumn : BoardColumnDeprecated;
- },
},
methods: {
...mapActions(['moveList', 'unsetError']),
@@ -95,8 +78,7 @@ export default {
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
@end="moveList"
>
- <component
- :is="boardColumnComponent"
+ <board-column
v-for="(list, index) in boardListsToUse"
:key="index"
ref="board"
@@ -118,10 +100,7 @@ export default {
:disabled="disabled"
/>
- <board-content-sidebar
- v-if="isSwimlanesOn || glFeatures.graphqlBoardLists"
- data-testid="issue-boards-sidebar"
- />
+ <board-content-sidebar v-if="isIssueBoard" data-testid="issue-boards-sidebar" />
<epic-board-content-sidebar v-else-if="isEpicBoard" data-testid="epic-boards-sidebar" />
</div>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 7a936e75676..e0105d63d99 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -96,7 +96,7 @@ export default {
<template #header>
<sidebar-todo-widget
class="gl-mt-3"
- :issuable-id="activeBoardItem.fullId"
+ :issuable-id="activeBoardItem.id"
:issuable-iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index a89f71504a9..e939f0c0ebe 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,8 +1,7 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
-import ListLabel from '~/boards/models/label';
-import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
+import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -189,7 +188,9 @@ export default {
issueBoardScopeMutationVariables() {
return {
weight: this.board.weight,
- assigneeId: this.board.assignee?.id || null,
+ assigneeId: this.board.assignee?.id
+ ? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
+ : null,
milestoneId: this.board.milestone?.id
? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
@@ -289,14 +290,10 @@ export default {
setBoardLabels(labels) {
labels.forEach((label) => {
if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
- this.board.labels.push(
- new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color,
- textColor: label.text_color,
- }),
- );
+ this.board.labels.push({
+ ...label,
+ textColor: label.text_color,
+ });
} else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
}
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 849492effab..47dffc985aa 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -208,7 +208,7 @@ export default {
newIndex = children.length;
}
- const getItemId = (el) => Number(el.dataset.itemId);
+ const getItemId = (el) => el.dataset.itemId;
// If item is being moved within the same list
if (from === to) {
@@ -234,7 +234,7 @@ export default {
}
this.moveItem({
- itemId: Number(itemId),
+ itemId,
itemIid,
itemPath,
fromListId: from.dataset.listId,
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
deleted file mode 100644
index fabaf7a85f5..00000000000
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ /dev/null
@@ -1,459 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { Sortable, MultiDrag } from 'sortablejs';
-import createFlash from '~/flash';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { sprintf, __ } from '~/locale';
-import eventHub from '../eventhub';
-import {
- getBoardSortableDefaultOptions,
- sortableStart,
- sortableEnd,
-} from '../mixins/sortable_default_options';
-import boardsStore from '../stores/boards_store';
-import boardCard from './board_card_deprecated.vue';
-import boardNewIssue from './board_new_issue_deprecated.vue';
-
-// This component is being replaced in favor of './board_list.vue' for GraphQL boards
-
-Sortable.mount(new MultiDrag());
-
-export default {
- name: 'BoardList',
- components: {
- boardCard,
- boardNewIssue,
- GlLoadingIcon,
- },
- props: {
- disabled: {
- type: Boolean,
- required: true,
- },
- list: {
- type: Object,
- required: true,
- },
- issues: {
- type: Array,
- required: true,
- },
- },
- data() {
- return {
- scrollOffset: 250,
- filters: boardsStore.state.filters,
- showCount: false,
- showIssueForm: false,
- };
- },
- computed: {
- paginatedIssueText() {
- return sprintf(__('Showing %{pageSize} of %{total} issues'), {
- pageSize: this.list.issues.length,
- total: this.list.issuesSize,
- });
- },
- issuesSizeExceedsMax() {
- return this.list.maxIssueCount > 0 && this.list.issuesSize > this.list.maxIssueCount;
- },
- loading() {
- return this.list.loading;
- },
- },
- watch: {
- filters: {
- handler() {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.loadingMore = false;
- this.$refs.list.scrollTop = 0;
- },
- deep: true,
- },
- issues() {
- this.$nextTick(() => {
- if (
- this.scrollHeight() <= this.listHeight() &&
- this.list.issuesSize > this.list.issues.length &&
- this.list.isExpanded
- ) {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.page += 1;
- this.list.getIssues(false).catch(() => {
- // TODO: handle request error
- });
- }
-
- if (this.scrollHeight() > Math.ceil(this.listHeight())) {
- this.showCount = true;
- } else {
- this.showCount = false;
- }
- });
- },
- 'list.id': {
- handler(id) {
- if (id) {
- eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
- }
- },
- },
- },
- created() {
- eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
- eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- },
- mounted() {
- const multiSelectOpts = {
- multiDrag: true,
- selectedClass: 'js-multi-select',
- animation: 500,
- };
-
- const options = getBoardSortableDefaultOptions({
- scroll: true,
- disabled: this.disabled,
- filter: '.board-list-count, .is-disabled',
- dataIdAttr: 'data-issue-id',
- removeCloneOnHide: false,
- ...multiSelectOpts,
- group: {
- name: 'issues',
- /**
- * Dynamically determine between which containers
- * items can be moved or copied as
- * Assignee lists (EE feature) require this behavior
- */
- pull: (to, from, dragEl, e) => {
- // As per Sortable's docs, `to` should provide
- // reference to exact sortable container on which
- // we're trying to drag element, but either it is
- // a library's bug or our markup structure is too complex
- // that `to` never points to correct container
- // See https://github.com/RubaXa/Sortable/issues/1037
- //
- // So we use `e.target` which is always accurate about
- // which element we're currently dragging our card upon
- // So from there, we can get reference to actual container
- // and thus the container type to enable Copy or Move
- if (e.target) {
- const containerEl =
- e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list');
- const toBoardType = containerEl.dataset.boardType;
- const cloneActions = {
- label: ['milestone', 'assignee', 'iteration'],
- assignee: ['milestone', 'label', 'iteration'],
- milestone: ['label', 'assignee', 'iteration'],
- iteration: ['label', 'assignee', 'milestone'],
- };
-
- if (toBoardType) {
- const fromBoardType = this.list.type;
- // For each list we check if the destination list is
- // a the list were we should clone the issue
- const shouldClone = Object.entries(cloneActions).some(
- (entry) => fromBoardType === entry[0] && entry[1].includes(toBoardType),
- );
-
- if (shouldClone) {
- return 'clone';
- }
- }
- }
-
- return true;
- },
- revertClone: true,
- },
- onStart: (e) => {
- const card = this.$refs.issue[e.oldIndex];
-
- card.showDetail = false;
-
- const { list } = card;
-
- const issue = list.findIssue(Number(e.item.dataset.issueId));
-
- boardsStore.startMoving(list, issue);
-
- this.$root.$emit(BV_HIDE_TOOLTIP);
-
- sortableStart();
- },
- onAdd: (e) => {
- const { items = [], newIndicies = [] } = e;
- if (items.length) {
- // Not using e.newIndex here instead taking a min of all
- // the newIndicies. Basically we have to find that during
- // a drop what is the index we're going to start putting
- // all the dropped elements from.
- const newIndex = Math.min(...newIndicies.map((obj) => obj.index).filter((i) => i !== -1));
- const issues = items.map((item) =>
- boardsStore.moving.list.findIssue(Number(item.dataset.issueId)),
- );
-
- boardsStore.moveMultipleIssuesToList({
- listFrom: boardsStore.moving.list,
- listTo: this.list,
- issues,
- newIndex,
- });
- } else {
- boardsStore.moveIssueToList(
- boardsStore.moving.list,
- this.list,
- boardsStore.moving.issue,
- e.newIndex,
- );
- this.$nextTick(() => {
- e.item.remove();
- });
- }
- },
- onUpdate: (e) => {
- const sortedArray = this.sortable.toArray().filter((id) => id !== '-1');
-
- const { items = [], newIndicies = [], oldIndicies = [] } = e;
- if (items.length) {
- const newIndex = Math.min(...newIndicies.map((obj) => obj.index));
- const issues = items.map((item) =>
- boardsStore.moving.list.findIssue(Number(item.dataset.issueId)),
- );
- boardsStore.moveMultipleIssuesInList({
- list: this.list,
- issues,
- oldIndicies: oldIndicies.map((obj) => obj.index),
- newIndex,
- idArray: sortedArray,
- });
- e.items.forEach((el) => {
- Sortable.utils.deselect(el);
- });
- boardsStore.clearMultiSelect();
- return;
- }
-
- boardsStore.moveIssueInList(
- this.list,
- boardsStore.moving.issue,
- e.oldIndex,
- e.newIndex,
- sortedArray,
- );
- },
- onEnd: (e) => {
- const { items = [], clones = [], to } = e;
-
- // This is not a multi select operation
- if (!items.length && !clones.length) {
- sortableEnd();
- return;
- }
-
- let toList;
- if (to) {
- const containerEl = to.closest('.js-board-list');
- toList = boardsStore.findList('id', Number(containerEl.dataset.board));
- }
-
- /**
- * onEnd is called irrespective if the cards were moved in the
- * same list or the other list. Don't remove items if it's same list.
- */
- const isSameList = toList && toList.id === this.list.id;
- if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) {
- const issues = items.map((item) => this.list.findIssue(Number(item.dataset.issueId)));
- if (
- issues.filter(Boolean).length &&
- !boardsStore.issuesAreContiguous(this.list, issues)
- ) {
- const indexes = [];
- const ids = this.list.issues.map((i) => i.id);
- issues.forEach((issue) => {
- const index = ids.indexOf(issue.id);
- if (index > -1) {
- indexes.push(index);
- }
- });
-
- // Descending sort because splice would cause index discrepancy otherwise
- const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1));
-
- sortedIndexes.forEach((i) => {
- /**
- * **setTimeout and splice each element one-by-one in a loop
- * is intended.**
- *
- * The problem here is all the indexes are in the list but are
- * non-contiguous. Due to that, when we splice all the indexes,
- * at once, Vue -- during a re-render -- is unable to find reference
- * nodes and the entire app crashes.
- *
- * If the indexes are contiguous, this piece of code is not
- * executed. If it is, this is a possible regression. Only when
- * issue indexes are far apart, this logic should ever kick in.
- */
- setTimeout(() => {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.issues.splice(i, 1);
- }, 0);
- });
- }
- }
-
- if (!toList) {
- createFlash({
- message: __('Something went wrong while performing the action.'),
- });
- }
-
- if (!isSameList) {
- boardsStore.clearMultiSelect();
-
- // Since Vue's list does not re-render the same keyed item, we'll
- // remove `multi-select` class to express it's unselected
- if (clones && clones.length) {
- clones.forEach((el) => el.classList.remove('multi-select'));
- }
-
- // Due to some bug which I am unable to figure out
- // Sortable does not deselect some pending items from the
- // source list.
- // We'll just do it forcefully here.
- Array.from(document.querySelectorAll('.js-multi-select') || []).forEach((item) => {
- Sortable.utils.deselect(item);
- });
-
- /**
- * SortableJS leaves all the moving items "as is" on the DOM.
- * Vue picks up and rehydrates the DOM, but we need to explicity
- * remove the "trash" items from the DOM.
- *
- * This is in parity to the logic on single item move from a list/in
- * a list. For reference, look at the implementation of onAdd method.
- */
- this.$nextTick(() => {
- if (items && items.length) {
- items.forEach((item) => {
- item.remove();
- });
- }
- });
- }
- sortableEnd();
- },
- onMove(e) {
- return !e.related.classList.contains('board-list-count');
- },
- onSelect(e) {
- const {
- item: { classList },
- } = e;
-
- if (
- classList &&
- classList.contains('js-multi-select') &&
- !classList.contains('multi-select')
- ) {
- Sortable.utils.deselect(e.item);
- }
- },
- onDeselect: (e) => {
- const {
- item: { dataset, classList },
- } = e;
-
- if (
- classList &&
- classList.contains('multi-select') &&
- !classList.contains('js-multi-select')
- ) {
- const issue = this.list.findIssue(Number(dataset.issueId));
- boardsStore.toggleMultiSelect(issue);
- }
- },
- });
-
- this.sortable = Sortable.create(this.$refs.list, options);
-
- // Scroll event on list to load more
- this.$refs.list.addEventListener('scroll', this.onScroll);
- },
- beforeDestroy() {
- eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
- eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- this.$refs.list.removeEventListener('scroll', this.onScroll);
- },
- methods: {
- listHeight() {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight() {
- return this.$refs.list.scrollHeight;
- },
- scrollTop() {
- return this.$refs.list.scrollTop + this.listHeight();
- },
- scrollToTop() {
- this.$refs.list.scrollTop = 0;
- },
- loadNextPage() {
- const getIssues = this.list.nextPage();
- const loadingDone = () => {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.loadingMore = false;
- };
-
- if (getIssues) {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.loadingMore = true;
- getIssues.then(loadingDone).catch(loadingDone);
- }
- },
- toggleForm() {
- this.showIssueForm = !this.showIssueForm;
- },
- onScroll() {
- if (!this.list.loadingMore && this.scrollTop() > this.scrollHeight() - this.scrollOffset) {
- this.loadNextPage();
- }
- },
- },
-};
-</script>
-
-<template>
- <div
- :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }"
- class="board-list-component position-relative h-100"
- data-qa-selector="board_list_cards_area"
- >
- <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
- <gl-loading-icon size="sm" />
- </div>
- <board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
- <ul
- v-show="!loading"
- ref="list"
- :data-board="list.id"
- :data-board-type="list.type"
- :class="{ 'is-smaller': showIssueForm, 'bg-danger-100': issuesSizeExceedsMax }"
- class="board-list w-100 h-100 list-unstyled mb-0 p-1 js-board-list"
- >
- <board-card
- v-for="(issue, index) in issues"
- ref="issue"
- :key="issue.id"
- :index="index"
- :list="list"
- :issue="issue"
- :disabled="disabled"
- />
- <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
- <gl-loading-icon v-show="list.loadingMore" size="sm" label="Loading more issues" />
- <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
- <span v-else>{{ paginatedIssueText }}</span>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 8d5f0f7eb89..dc5313b1bf6 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -201,7 +201,7 @@ export default {
});
},
addToLocalStorage() {
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
localStorage.setItem(`${this.uniqueKey}.collapsed`, this.list.collapsed);
}
},
diff --git a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue b/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
deleted file mode 100644
index bc29728fc55..00000000000
--- a/app/assets/javascripts/boards/components/board_list_header_deprecated.vue
+++ /dev/null
@@ -1,361 +0,0 @@
-<script>
-import {
- GlButton,
- GlButtonGroup,
- GlLabel,
- GlTooltip,
- GlIcon,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { n__, s__ } from '~/locale';
-import sidebarEventHub from '~/sidebar/event_hub';
-import AccessorUtilities from '../../lib/utils/accessor';
-import { inactiveId, LIST, ListType } from '../constants';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
-import IssueCount from './item_count.vue';
-
-// This component is being replaced in favor of './board_list_header.vue' for GraphQL boards
-
-export default {
- components: {
- GlButtonGroup,
- GlButton,
- GlLabel,
- GlTooltip,
- GlIcon,
- GlSprintf,
- IssueCount,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: {
- currentUserId: {
- default: null,
- },
- boardId: {
- default: '',
- },
- },
- props: {
- list: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- disabled: {
- type: Boolean,
- required: true,
- },
- isSwimlanesHeader: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- weightFeatureAvailable: false,
- };
- },
- computed: {
- ...mapState(['activeId']),
- isLoggedIn() {
- return Boolean(this.currentUserId);
- },
- listType() {
- return this.list.type;
- },
- listAssignee() {
- return this.list?.assignee?.username || '';
- },
- listTitle() {
- return this.list?.label?.description || this.list.title || '';
- },
- showListHeaderButton() {
- return !this.disabled && this.listType !== ListType.closed;
- },
- showMilestoneListDetails() {
- return this.list.type === 'milestone' && this.list.milestone && this.showListDetails;
- },
- showAssigneeListDetails() {
- return this.list.type === 'assignee' && this.showListDetails;
- },
- showIterationListDetails() {
- return this.listType === ListType.iteration && this.showListDetails;
- },
- showListDetails() {
- return this.list.isExpanded || !this.isSwimlanesHeader;
- },
- showListHeaderActions() {
- if (this.isLoggedIn) {
- return this.isNewIssueShown || this.isSettingsShown;
- }
- return false;
- },
- issuesCount() {
- return this.list.issuesSize;
- },
- issuesTooltipLabel() {
- return n__(`%d issue`, `%d issues`, this.issuesCount);
- },
- chevronTooltip() {
- return this.list.isExpanded ? s__('Boards|Collapse') : s__('Boards|Expand');
- },
- chevronIcon() {
- return this.list.isExpanded ? 'chevron-right' : 'chevron-down';
- },
- isNewIssueShown() {
- return this.listType === ListType.backlog || this.showListHeaderButton;
- },
- isSettingsShown() {
- return (
- this.listType !== ListType.backlog && this.showListHeaderButton && this.list.isExpanded
- );
- },
- uniqueKey() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `boards.${this.boardId}.${this.listType}.${this.list.id}`;
- },
- collapsedTooltipTitle() {
- return this.listTitle || this.listAssignee;
- },
- },
- methods: {
- ...mapActions(['setActiveId']),
- openSidebarSettings() {
- if (this.activeId === inactiveId) {
- sidebarEventHub.$emit('sidebar.closeAll');
- }
-
- this.setActiveId({ id: this.list.id, sidebarType: LIST });
- },
- showScopedLabels(label) {
- return boardsStore.scopedLabels.enabled && isScopedLabel(label);
- },
-
- showNewIssueForm() {
- eventHub.$emit(`toggle-issue-form-${this.list.id}`);
- },
- toggleExpanded() {
- // eslint-disable-next-line vue/no-mutating-props
- this.list.isExpanded = !this.list.isExpanded;
-
- if (!this.isLoggedIn) {
- this.addToLocalStorage();
- } else {
- this.updateListFunction();
- }
-
- // When expanding/collapsing, the tooltip on the caret button sometimes stays open.
- // Close all tooltips manually to prevent dangling tooltips.
- this.$root.$emit(BV_HIDE_TOOLTIP);
- },
- addToLocalStorage() {
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
- localStorage.setItem(`${this.uniqueKey}.expanded`, this.list.isExpanded);
- }
- },
- updateListFunction() {
- this.list.update();
- },
- },
-};
-</script>
-
-<template>
- <header
- :class="{
- 'has-border': list.label && list.label.color,
- 'gl-h-full': !list.isExpanded,
- 'board-inner gl-rounded-top-left-base gl-rounded-top-right-base': isSwimlanesHeader,
- }"
- :style="{ borderTopColor: list.label && list.label.color ? list.label.color : null }"
- class="board-header gl-relative"
- data-qa-selector="board_list_header"
- data-testid="board-list-header"
- >
- <h3
- :class="{
- 'user-can-drag': !disabled && !list.preset,
- 'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
- 'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
- 'gl-py-2': !list.isExpanded && isSwimlanesHeader,
- 'gl-flex-direction-column': !list.isExpanded,
- }"
- class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle"
- >
- <gl-button
- v-if="list.isExpandable"
- v-gl-tooltip.hover
- :aria-label="chevronTooltip"
- :title="chevronTooltip"
- :icon="chevronIcon"
- class="board-title-caret no-drag gl-cursor-pointer"
- category="tertiary"
- size="small"
- @click="toggleExpanded"
- />
- <!-- The following is only true in EE and if it is a milestone -->
- <span
- v-if="showMilestoneListDetails"
- aria-hidden="true"
- class="milestone-icon"
- :class="{
- 'gl-mt-3 gl-rotate-90': !list.isExpanded,
- 'gl-mr-2': list.isExpanded,
- }"
- >
- <gl-icon name="timer" />
- </span>
-
- <span
- v-if="showIterationListDetails"
- aria-hidden="true"
- :class="{
- 'gl-mt-3 gl-rotate-90': !list.isExpanded,
- 'gl-mr-2': list.isExpanded,
- }"
- >
- <gl-icon name="iteration" />
- </span>
-
- <a
- v-if="showAssigneeListDetails"
- :href="list.assignee.path"
- class="user-avatar-link js-no-trigger"
- :class="{
- 'gl-mt-3 gl-rotate-90': !list.isExpanded,
- }"
- >
- <img
- v-gl-tooltip.hover.bottom
- :title="listAssignee"
- :alt="list.assignee.name"
- :src="list.assignee.avatar"
- class="avatar s20"
- height="20"
- width="20"
- />
- </a>
- <div
- class="board-title-text"
- :class="{
- 'gl-display-none': !list.isExpanded && isSwimlanesHeader,
- 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded,
- 'gl-flex-grow-1': list.isExpanded,
- }"
- >
- <span
- v-if="list.type !== 'label'"
- v-gl-tooltip.hover
- :class="{
- 'gl-display-block': !list.isExpanded || list.type === 'milestone',
- }"
- :title="listTitle"
- class="board-title-main-text gl-text-truncate"
- >
- {{ list.title }}
- </span>
- <span
- v-if="list.type === 'assignee'"
- class="gl-ml-2 gl-font-weight-normal gl-text-gray-500"
- :class="{ 'gl-display-none': !list.isExpanded }"
- >
- @{{ listAssignee }}
- </span>
- <gl-label
- v-if="list.type === 'label'"
- v-gl-tooltip.hover.bottom
- :background-color="list.label.color"
- :description="list.label.description"
- :scoped="showScopedLabels(list.label)"
- :size="!list.isExpanded ? 'sm' : ''"
- :title="list.label.title"
- />
- </div>
-
- <span
- v-if="isSwimlanesHeader && !list.isExpanded"
- ref="collapsedInfo"
- aria-hidden="true"
- class="board-header-collapsed-info-icon gl-mt-2 gl-cursor-pointer gl-text-gray-500"
- >
- <gl-icon name="information" />
- </span>
- <gl-tooltip v-if="isSwimlanesHeader && !list.isExpanded" :target="() => $refs.collapsedInfo">
- <div class="gl-font-weight-bold gl-pb-2">{{ collapsedTooltipTitle }}</div>
- <div v-if="list.maxIssueCount !== 0">
- &#8226;
- <gl-sprintf :message="__('%{issuesSize} with a limit of %{maxIssueCount}')">
- <template #issuesSize>{{ issuesTooltipLabel }}</template>
- <template #maxIssueCount>{{ list.maxIssueCount }}</template>
- </gl-sprintf>
- </div>
- <div v-else>&#8226; {{ issuesTooltipLabel }}</div>
- <div v-if="weightFeatureAvailable">
- &#8226;
- <gl-sprintf :message="__('%{totalWeight} total weight')">
- <template #totalWeight>{{ list.totalWeight }}</template>
- </gl-sprintf>
- </div>
- </gl-tooltip>
-
- <div
- class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
- :class="{
- 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
- 'gl-p-0': !list.isExpanded,
- }"
- >
- <span class="gl-display-inline-flex">
- <gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
- <span ref="issueCount" class="issue-count-badge-count">
- <gl-icon class="gl-mr-2" name="issues" />
- <issue-count :items-size="issuesCount" :max-issue-count="list.maxIssueCount" />
- </span>
- <!-- The following is only true in EE. -->
- <template v-if="weightFeatureAvailable">
- <gl-tooltip :target="() => $refs.weightTooltip" :title="weightCountToolTip" />
- <span ref="weightTooltip" class="gl-display-inline-flex gl-ml-3">
- <gl-icon class="gl-mr-2" name="weight" />
- {{ list.totalWeight }}
- </span>
- </template>
- </span>
- </div>
- <gl-button-group v-if="showListHeaderActions" class="board-list-button-group pl-2">
- <gl-button
- v-if="isNewIssueShown"
- ref="newIssueBtn"
- v-gl-tooltip.hover
- :class="{
- 'gl-display-none': !list.isExpanded,
- }"
- :aria-label="__('New issue')"
- :title="__('New issue')"
- class="issue-count-badge-add-button no-drag"
- icon="plus"
- @click="showNewIssueForm"
- />
-
- <gl-button
- v-if="isSettingsShown"
- ref="settingsBtn"
- v-gl-tooltip.hover
- :aria-label="__('List settings')"
- class="no-drag js-board-settings-button"
- :title="__('List settings')"
- icon="settings"
- @click="openSidebarSettings"
- />
- <gl-tooltip :target="() => $refs.settingsBtn">{{ __('List settings') }}</gl-tooltip>
- </gl-button-group>
- </h3>
- </header>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue b/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
deleted file mode 100644
index a25b436b8de..00000000000
--- a/app/assets/javascripts/boards/components/board_new_issue_deprecated.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
-import { getMilestone } from 'ee_else_ce/boards/boards_util';
-import ListIssue from 'ee_else_ce/boards/models/issue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import eventHub from '../eventhub';
-import boardsStore from '../stores/boards_store';
-import ProjectSelect from './project_select_deprecated.vue';
-
-// This component is being replaced in favor of './board_new_issue.vue' for GraphQL boards
-
-export default {
- name: 'BoardNewIssueDeprecated',
- components: {
- ProjectSelect,
- GlButton,
- },
- mixins: [glFeatureFlagMixin()],
- inject: ['groupId'],
- props: {
- list: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- title: '',
- error: false,
- selectedProject: {},
- };
- },
- computed: {
- ...mapGetters(['isGroupBoard']),
- disabled() {
- if (this.isGroupBoard) {
- return this.title === '' || !this.selectedProject.name;
- }
- return this.title === '';
- },
- },
- mounted() {
- this.$refs.input.focus();
- eventHub.$on('setSelectedProject', this.setSelectedProject);
- },
- methods: {
- submit(e) {
- e.preventDefault();
- if (this.title.trim() === '') return Promise.resolve();
-
- this.error = false;
-
- const labels = this.list.label ? [this.list.label] : [];
- const assignees = this.list.assignee ? [this.list.assignee] : [];
- const milestone = getMilestone(this.list);
-
- const { weightFeatureAvailable } = boardsStore;
- const { weight } = weightFeatureAvailable ? boardsStore.state.currentBoard : {};
-
- const issue = new ListIssue({
- title: this.title,
- labels,
- subscribed: true,
- assignees,
- milestone,
- project_id: this.selectedProject.id,
- weight,
- });
-
- eventHub.$emit(`scroll-board-list-${this.list.id}`);
- this.cancel();
-
- return this.list
- .newIssue(issue)
- .then(() => {
- boardsStore.setIssueDetail(issue);
- boardsStore.setListDetail(this.list);
- })
- .catch(() => {
- this.list.removeIssue(issue);
-
- // Show error message
- this.error = true;
- });
- },
- cancel() {
- this.title = '';
- eventHub.$emit(`toggle-issue-form-${this.list.id}`);
- },
- setSelectedProject(selectedProject) {
- this.selectedProject = selectedProject;
- },
- },
-};
-</script>
-
-<template>
- <div class="board-new-issue-form">
- <div class="board-card position-relative p-3 rounded">
- <form @submit="submit($event)">
- <div v-if="error" class="flash-container">
- <div class="flash-alert">{{ __('An error occurred. Please try again.') }}</div>
- </div>
- <label :for="list.id + '-title'" class="label-bold">{{ __('Title') }}</label>
- <input
- :id="list.id + '-title'"
- ref="input"
- v-model="title"
- class="form-control"
- type="text"
- name="issue_title"
- autocomplete="off"
- />
- <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
- <div class="clearfix gl-mt-3">
- <gl-button
- ref="submitButton"
- :disabled="disabled"
- class="float-left js-no-auto-disable"
- variant="success"
- category="primary"
- type="submit"
- >{{ __('Create issue') }}</gl-button
- >
- <gl-button
- ref="cancelButton"
- class="float-right"
- type="button"
- variant="default"
- @click="cancel"
- >{{ __('Cancel') }}</gl-button
- >
- </div>
- </form>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index c089a6a39af..6b7c08d05a5 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -3,7 +3,6 @@ import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -23,7 +22,7 @@ export default {
import('ee_component/boards/components/board_settings_list_types.vue'),
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['canAdminList'],
+ inject: ['canAdminList', 'scopedLabelsAvailable'],
inheritAttrs: false,
data() {
return {
@@ -31,20 +30,13 @@ export default {
};
},
computed: {
- ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']),
+ ...mapGetters(['isSidebarOpen', 'isEpicBoard']),
...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
return this.glFeatures.wipLimits && !this.isEpicBoard;
},
activeList() {
- /*
- Warning: Though a computed property it is not reactive because we are
- referencing a List Model class. Reactivity only applies to plain JS objects
- */
- if (this.shouldUseGraphQL || this.isEpicBoard) {
- return this.boardLists[this.activeId];
- }
- return boardsStore.state.lists.find(({ id }) => id === this.activeId);
+ return this.boardLists[this.activeId] || {};
},
activeListLabel() {
return this.activeList.label;
@@ -68,17 +60,13 @@ export default {
methods: {
...mapActions(['unsetActiveId', 'removeList']),
showScopedLabels(label) {
- return boardsStore.scopedLabels.enabled && isScopedLabel(label);
+ return this.scopedLabelsAvailable && isScopedLabel(label);
},
deleteBoard() {
// eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to remove this list?'))) {
- if (this.shouldUseGraphQL || this.isEpicBoard) {
- this.track('click_button', { label: 'remove_list' });
- this.removeList(this.activeId);
- } else {
- this.activeList.destroy();
- }
+ this.track('click_button', { label: 'remove_list' });
+ this.removeList(this.activeId);
this.unsetActiveId();
}
},
@@ -93,9 +81,26 @@ export default {
v-bind="$attrs"
class="js-board-settings-sidebar gl-absolute"
:open="isSidebarOpen"
+ variant="sidebar"
@close="unsetActiveId"
>
- <template #title>{{ $options.listSettingsText }}</template>
+ <template #title>
+ <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
+ {{ $options.listSettingsText }}
+ </h2>
+ </template>
+ <template #header>
+ <div v-if="canAdminList && activeList.id" class="gl-mt-3">
+ <gl-button
+ variant="danger"
+ category="secondary"
+ size="small"
+ data-testid="remove-list"
+ @click.stop="deleteBoard"
+ >{{ __('Remove list') }}
+ </gl-button>
+ </div>
+ </template>
<template v-if="isSidebarOpen">
<div v-if="boardListType === ListType.label">
<label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
@@ -115,16 +120,6 @@ export default {
v-if="isWipLimitsOn"
:max-issue-count="activeList.maxIssueCount"
/>
- <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4">
- <gl-button
- variant="danger"
- category="secondary"
- icon="remove"
- data-testid="remove-list"
- @click.stop="deleteBoard"
- >{{ __('Remove list') }}
- </gl-button>
- </div>
</template>
</gl-drawer>
</mounting-portal>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
deleted file mode 100644
index 21a34182369..00000000000
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ /dev/null
@@ -1,115 +0,0 @@
-// This is a true violation of @gitlab/no-runtime-template-compiler, as it
-// relies on app/views/shared/boards/components/_sidebar.html.haml for its
-// template.
-/* eslint-disable no-new, @gitlab/no-runtime-template-compiler */
-
-import { GlLabel } from '@gitlab/ui';
-import $ from 'jquery';
-import Vue from 'vue';
-import DueDateSelectors from '~/due_date_select';
-import IssuableContext from '~/issuable_context';
-import LabelsSelect from '~/labels_select';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { sprintf, __ } from '~/locale';
-import MilestoneSelect from '~/milestone_select';
-import Sidebar from '~/right_sidebar';
-import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
-import Assignees from '~/sidebar/components/assignees/assignees.vue';
-import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
-import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
-import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
-import eventHub from '~/sidebar/event_hub';
-import boardsStore from '../stores/boards_store';
-
-export default Vue.extend({
- components: {
- AssigneeTitle,
- Assignees,
- GlLabel,
- SidebarEpicsSelect: () =>
- import('ee_component/sidebar/components/sidebar_item_epics_select.vue'),
- Subscriptions,
- TimeTracker,
- SidebarAssigneesWidget,
- },
- props: {
- currentUser: {
- type: Object,
- default: () => ({}),
- required: false,
- },
- },
- data() {
- return {
- detail: boardsStore.detail,
- issue: {},
- list: {},
- loadingAssignees: false,
- timeTrackingLimitToHours: boardsStore.timeTracking.limitToHours,
- };
- },
- computed: {
- showSidebar() {
- return Object.keys(this.issue).length;
- },
- milestoneTitle() {
- return this.issue.milestone ? this.issue.milestone.title : __('No milestone');
- },
- canRemove() {
- return !this.list?.preset;
- },
- hasLabels() {
- return this.issue.labels && this.issue.labels.length;
- },
- labelDropdownTitle() {
- return this.hasLabels
- ? sprintf(__('%{firstLabel} +%{labelCount} more'), {
- firstLabel: this.issue.labels[0].title,
- labelCount: this.issue.labels.length - 1,
- })
- : __('Label');
- },
- selectedLabels() {
- return this.hasLabels ? this.issue.labels.map((l) => l.title).join(',') : '';
- },
- },
- watch: {
- detail: {
- handler() {
- if (this.issue.id !== this.detail.issue.id) {
- $('.js-issue-board-sidebar', this.$el).each((i, el) => {
- $(el).data('deprecatedJQueryDropdown').clearMenu();
- });
- }
-
- this.issue = this.detail.issue;
- this.list = this.detail.list;
- },
- deep: true,
- },
- },
- created() {
- eventHub.$on('sidebar.closeAll', this.closeSidebar);
- },
- beforeDestroy() {
- eventHub.$off('sidebar.closeAll', this.closeSidebar);
- },
- mounted() {
- new IssuableContext(this.currentUser);
- new MilestoneSelect();
- new DueDateSelectors();
- new LabelsSelect();
- new Sidebar();
- },
- methods: {
- closeSidebar() {
- this.detail.issue = {};
- },
- setAssignees({ assignees }) {
- boardsStore.detail.issue.setAssignees(assignees);
- },
- showScopedLabels(label) {
- return boardsStore.scopedLabels.enabled && isScopedLabel(label);
- },
- },
-});
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
deleted file mode 100644
index c1536dff2c6..00000000000
--- a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
+++ /dev/null
@@ -1,360 +0,0 @@
-<script>
-import {
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlModalDirective,
-} from '@gitlab/ui';
-import { throttle } from 'lodash';
-import { mapGetters, mapState } from 'vuex';
-
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-
-import groupQuery from '../graphql/group_boards.query.graphql';
-import projectQuery from '../graphql/project_boards.query.graphql';
-
-import boardsStore from '../stores/boards_store';
-import BoardForm from './board_form.vue';
-
-const MIN_BOARDS_TO_VIEW_RECENT = 10;
-
-export default {
- name: 'BoardsSelector',
- components: {
- BoardForm,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- },
- directives: {
- GlModalDirective,
- },
- props: {
- currentBoard: {
- type: Object,
- required: true,
- },
- throttleDuration: {
- type: Number,
- default: 200,
- required: false,
- },
- boardBaseUrl: {
- type: String,
- required: true,
- },
- hasMissingBoards: {
- type: Boolean,
- required: true,
- },
- canAdminBoard: {
- type: Boolean,
- required: true,
- },
- multipleIssueBoardsAvailable: {
- type: Boolean,
- required: true,
- },
- labelsPath: {
- type: String,
- required: true,
- },
- labelsWebUrl: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- groupId: {
- type: Number,
- required: true,
- },
- scopedIssueBoardFeatureEnabled: {
- type: Boolean,
- required: true,
- },
- weights: {
- type: Array,
- required: true,
- },
- enabledScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- hasScrollFade: false,
- loadingBoards: 0,
- loadingRecentBoards: false,
- scrollFadeInitialized: false,
- boards: [],
- recentBoards: [],
- state: boardsStore.state,
- throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
- contentClientHeight: 0,
- maxPosition: 0,
- store: boardsStore,
- filterTerm: '',
- };
- },
- computed: {
- ...mapState(['boardType']),
- ...mapGetters(['isGroupBoard']),
- parentType() {
- return this.boardType;
- },
- loading() {
- return this.loadingRecentBoards || Boolean(this.loadingBoards);
- },
- currentPage() {
- return this.state.currentPage;
- },
- filteredBoards() {
- return this.boards.filter((board) =>
- board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
- );
- },
- board() {
- return this.state.currentBoard;
- },
- showDelete() {
- return this.boards.length > 1;
- },
- scrollFadeClass() {
- return {
- 'fade-out': !this.hasScrollFade,
- };
- },
- showRecentSection() {
- return (
- this.recentBoards.length &&
- this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
- !this.filterTerm.length
- );
- },
- },
- watch: {
- filteredBoards() {
- this.scrollFadeInitialized = false;
- this.$nextTick(this.setScrollFade);
- },
- },
- created() {
- boardsStore.setCurrentBoard(this.currentBoard);
- },
- methods: {
- showPage(page) {
- boardsStore.showPage(page);
- },
- cancel() {
- this.showPage('');
- },
- loadBoards(toggleDropdown = true) {
- if (toggleDropdown && this.boards.length > 0) {
- return;
- }
-
- this.$apollo.addSmartQuery('boards', {
- variables() {
- return { fullPath: this.state.endpoints.fullPath };
- },
- query() {
- return this.isGroupBoard ? groupQuery : projectQuery;
- },
- loadingKey: 'loadingBoards',
- update(data) {
- if (!data?.[this.parentType]) {
- return [];
- }
- return data[this.parentType].boards.edges.map(({ node }) => ({
- id: getIdFromGraphQLId(node.id),
- name: node.name,
- }));
- },
- });
-
- this.loadingRecentBoards = true;
- boardsStore
- .recentBoards()
- .then((res) => {
- this.recentBoards = res.data;
- })
- .catch((err) => {
- /**
- * If user is unauthorized we'd still want to resolve the
- * request to display all boards.
- */
- if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
- this.recentBoards = []; // recent boards are empty
- return;
- }
- throw err;
- })
- .then(() => this.$nextTick()) // Wait for boards list in DOM
- .then(() => {
- this.setScrollFade();
- })
- .catch(() => {})
- .finally(() => {
- this.loadingRecentBoards = false;
- });
- },
- isScrolledUp() {
- const { content } = this.$refs;
-
- if (!content) {
- return false;
- }
-
- const currentPosition = this.contentClientHeight + content.scrollTop;
-
- return currentPosition < this.maxPosition;
- },
- initScrollFade() {
- const { content } = this.$refs;
-
- if (!content) {
- return;
- }
-
- this.scrollFadeInitialized = true;
-
- this.contentClientHeight = content.clientHeight;
- this.maxPosition = content.scrollHeight;
- },
- setScrollFade() {
- if (!this.scrollFadeInitialized) this.initScrollFade();
-
- this.hasScrollFade = this.isScrolledUp();
- },
- },
-};
-</script>
-
-<template>
- <div class="boards-switcher js-boards-selector gl-mr-3">
- <span class="boards-selector-wrapper js-boards-selector-wrapper">
- <gl-dropdown
- data-qa-selector="boards_dropdown"
- toggle-class="dropdown-menu-toggle js-dropdown-toggle"
- menu-class="flex-column dropdown-extended-height"
- :text="board.name"
- @show="loadBoards"
- >
- <p class="gl-new-dropdown-header-top" @mousedown.prevent>
- {{ s__('IssueBoards|Switch board') }}
- </p>
- <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
-
- <div
- v-if="!loading"
- ref="content"
- data-qa-selector="boards_dropdown_content"
- class="dropdown-content flex-fill"
- @scroll.passive="throttledSetScrollFade"
- >
- <gl-dropdown-item
- v-show="filteredBoards.length === 0"
- class="gl-pointer-events-none text-secondary"
- >
- {{ s__('IssueBoards|No matching boards found') }}
- </gl-dropdown-item>
-
- <gl-dropdown-section-header v-if="showRecentSection">
- {{ __('Recent') }}
- </gl-dropdown-section-header>
-
- <template v-if="showRecentSection">
- <gl-dropdown-item
- v-for="recentBoard in recentBoards"
- :key="`recent-${recentBoard.id}`"
- class="js-dropdown-item"
- :href="`${boardBaseUrl}/${recentBoard.id}`"
- >
- {{ recentBoard.name }}
- </gl-dropdown-item>
- </template>
-
- <gl-dropdown-divider v-if="showRecentSection" />
-
- <gl-dropdown-section-header v-if="showRecentSection">
- {{ __('All') }}
- </gl-dropdown-section-header>
-
- <gl-dropdown-item
- v-for="otherBoard in filteredBoards"
- :key="otherBoard.id"
- class="js-dropdown-item"
- :href="`${boardBaseUrl}/${otherBoard.id}`"
- >
- {{ otherBoard.name }}
- </gl-dropdown-item>
-
- <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
- {{
- s__(
- 'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
- )
- }}
- </gl-dropdown-item>
- </div>
-
- <div
- v-show="filteredBoards.length > 0"
- class="dropdown-content-faded-mask"
- :class="scrollFadeClass"
- ></div>
-
- <gl-loading-icon v-if="loading" size="sm" />
-
- <div v-if="canAdminBoard">
- <gl-dropdown-divider />
-
- <gl-dropdown-item
- v-if="multipleIssueBoardsAvailable"
- v-gl-modal-directive="'board-config-modal'"
- data-qa-selector="create_new_board_button"
- @click.prevent="showPage('new')"
- >
- {{ s__('IssueBoards|Create new board') }}
- </gl-dropdown-item>
-
- <gl-dropdown-item
- v-if="showDelete"
- v-gl-modal-directive="'board-config-modal'"
- class="text-danger js-delete-board"
- @click.prevent="showPage('delete')"
- >
- {{ s__('IssueBoards|Delete board') }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
-
- <board-form
- v-if="currentPage"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :project-id="projectId"
- :group-id="groupId"
- :can-admin-board="canAdminBoard"
- :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
- :weights="weights"
- :enable-scoped-labels="enabledScopedLabels"
- :current-board="currentBoard"
- :current-page="state.currentPage"
- @cancel="cancel"
- />
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 30e304b8a65..f39e4d90357 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -15,11 +15,6 @@ export default {
},
mixins: [Tracking.mixin()],
props: {
- boardsStore: {
- type: Object,
- required: false,
- default: null,
- },
canAdminList: {
type: Boolean,
required: true,
@@ -41,9 +36,6 @@ export default {
showPage() {
this.track('click_button', { label: 'edit_board' });
eventHub.$emit('showBoardModal', formType.edit);
- if (this.boardsStore) {
- this.boardsStore.showPage(formType.edit);
- }
},
},
};
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
index 5206db05410..b6c5ef955c6 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -6,6 +6,7 @@ import issueBoardFilters from '~/boards/issue_board_filters';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
+import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -63,17 +64,17 @@ export default {
return [
{
- icon: 'labels',
- title: label,
- type: 'label_name',
+ icon: 'user',
+ title: assignee,
+ type: 'assignee_username',
operators: [
{ value: '=', description: is },
{ value: '!=', description: isNot },
],
- token: LabelToken,
- unique: false,
- symbol: '~',
- fetchLabels,
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ preloadedAuthors: this.preloadedAuthors(),
},
{
icon: 'pencil',
@@ -90,17 +91,27 @@ export default {
preloadedAuthors: this.preloadedAuthors(),
},
{
- icon: 'user',
- title: assignee,
- type: 'assignee_username',
+ icon: 'labels',
+ title: label,
+ type: 'label_name',
operators: [
{ value: '=', description: is },
{ value: '!=', description: isNot },
],
- token: AuthorToken,
+ token: LabelToken,
+ unique: false,
+ symbol: '~',
+ fetchLabels,
+ },
+ {
+ type: 'milestone_title',
+ title: milestone,
+ icon: 'clock',
+ symbol: '%',
+ token: MilestoneToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: this.preloadedAuthors(),
+ defaultMilestones: DEFAULT_MILESTONES_GRAPHQL,
+ fetchMilestones: this.fetchMilestones,
},
{
icon: 'issues',
@@ -115,16 +126,6 @@ export default {
],
},
{
- type: 'milestone_title',
- title: milestone,
- icon: 'clock',
- symbol: '%',
- token: MilestoneToken,
- unique: true,
- defaultMilestones: [], // todo: https://gitlab.com/gitlab-org/gitlab/-/issues/337044#note_640010094
- fetchMilestones: this.fetchMilestones,
- },
- {
type: 'weight',
title: weight,
icon: 'weight',
diff --git a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue b/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
deleted file mode 100644
index 6e90731cc2f..00000000000
--- a/app/assets/javascripts/boards/components/issue_card_inner_deprecated.vue
+++ /dev/null
@@ -1,247 +0,0 @@
-<script>
-import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { sortBy } from 'lodash';
-import { mapState } from 'vuex';
-import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import { sprintf, __, n__ } from '~/locale';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
-import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import boardsStore from '../stores/boards_store';
-import IssueDueDate from './issue_due_date.vue';
-import IssueTimeEstimate from './issue_time_estimate_deprecated.vue';
-
-export default {
- components: {
- GlLabel,
- GlIcon,
- UserAvatarLink,
- TooltipOnTruncate,
- IssueDueDate,
- IssueTimeEstimate,
- IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [boardCardInner],
- inject: ['groupId', 'rootPath'],
- props: {
- issue: {
- type: Object,
- required: true,
- },
- list: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- updateFilters: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- limitBeforeCounter: 2,
- maxRender: 3,
- maxCounter: 99,
- };
- },
- computed: {
- ...mapState(['isShowingLabels']),
- numberOverLimit() {
- return this.issue.assignees.length - this.limitBeforeCounter;
- },
- assigneeCounterTooltip() {
- const { numberOverLimit, maxCounter } = this;
- const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
- return sprintf(__('%{count} more assignees'), { count });
- },
- assigneeCounterLabel() {
- if (this.numberOverLimit > this.maxCounter) {
- return `${this.maxCounter}+`;
- }
-
- return `+${this.numberOverLimit}`;
- },
- shouldRenderCounter() {
- if (this.issue.assignees.length <= this.maxRender) {
- return false;
- }
-
- return this.issue.assignees.length > this.numberOverLimit;
- },
- issueId() {
- if (this.issue.iid) {
- return `#${this.issue.iid}`;
- }
- return false;
- },
- showLabelFooter() {
- return this.isShowingLabels && this.issue.labels.find(this.showLabel);
- },
- issueReferencePath() {
- const { referencePath, groupId } = this.issue;
- return !groupId ? referencePath.split('#')[0] : null;
- },
- orderedLabels() {
- return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
- },
- blockedLabel() {
- if (this.issue.blockedByCount) {
- return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.issue.blockedByCount);
- }
- return __('Blocked issue');
- },
- assignees() {
- return this.issue.assignees.filter((_, index) => this.shouldRenderAssignee(index));
- },
- },
- methods: {
- isIndexLessThanlimit(index) {
- return index < this.limitBeforeCounter;
- },
- shouldRenderAssignee(index) {
- // Eg. maxRender is 4,
- // Render up to all 4 assignees if there are only 4 assigness
- // Otherwise render up to the limitBeforeCounter
- if (this.issue.assignees.length <= this.maxRender) {
- return index < this.maxRender;
- }
-
- return index < this.limitBeforeCounter;
- },
- assigneeUrl(assignee) {
- if (!assignee) return '';
- return `${this.rootPath}${assignee.username}`;
- },
- avatarUrlTitle(assignee) {
- return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name });
- },
- showLabel(label) {
- if (!label.id) return false;
- return true;
- },
- isNonListLabel(label) {
- return label.id && !(this.list.type === 'label' && this.list.title === label.title);
- },
- filterByLabel(label) {
- if (!this.updateFilters) return;
- const labelTitle = encodeURIComponent(label.title);
- const filter = `label_name[]=${labelTitle}`;
-
- boardsStore.toggleFilter(filter);
- },
- showScopedLabel(label) {
- return boardsStore.scopedLabels.enabled && isScopedLabel(label);
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="gl-display-flex" dir="auto">
- <h4 class="board-card-title gl-mb-0 gl-mt-0">
- <gl-icon
- v-if="issue.blocked"
- v-gl-tooltip
- name="issue-block"
- :title="blockedLabel"
- class="issue-blocked-icon gl-mr-2"
- :aria-label="blockedLabel"
- data-testid="issue-blocked-icon"
- />
- <gl-icon
- v-if="issue.confidential"
- v-gl-tooltip
- name="eye-slash"
- :title="__('Confidential')"
- class="confidential-icon gl-mr-2"
- :aria-label="__('Confidential')"
- />
- <a
- :href="issue.path || issue.webUrl || ''"
- :title="issue.title"
- class="js-no-trigger"
- @mousemove.stop
- >{{ issue.title }}</a
- >
- </h4>
- </div>
- <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
- <template v-for="label in orderedLabels">
- <gl-label
- :key="label.id"
- :background-color="label.color"
- :title="label.title"
- :description="label.description"
- size="sm"
- :scoped="showScopedLabel(label)"
- @click="filterByLabel(label)"
- />
- </template>
- </div>
- <div
- class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end"
- >
- <div
- class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
- >
- <span
- v-if="issue.referencePath"
- class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
- >
- <tooltip-on-truncate
- v-if="issueReferencePath"
- :title="issueReferencePath"
- placement="bottom"
- class="board-issue-path gl-text-truncate gl-font-weight-bold"
- >{{ issueReferencePath }}</tooltip-on-truncate
- >
- #{{ issue.iid }}
- </span>
- <span class="board-info-items gl-mt-3 gl-display-inline-block">
- <issue-due-date
- v-if="issue.dueDate"
- :date="issue.dueDate"
- :closed="issue.closed || Boolean(issue.closedAt)"
- />
- <issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
- <issue-card-weight
- v-if="validIssueWeight(issue)"
- :weight="issue.weight"
- @click="filterByWeight(issue.weight)"
- />
- </span>
- </div>
- <div class="board-card-assignee gl-display-flex">
- <user-avatar-link
- v-for="assignee in assignees"
- :key="assignee.id"
- :link-href="assigneeUrl(assignee)"
- :img-alt="avatarUrlTitle(assignee)"
- :img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url"
- :img-size="24"
- class="js-no-trigger"
- tooltip-placement="bottom"
- >
- <span class="js-assignee-tooltip">
- <span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
- {{ assignee.name }}
- <span class="text-white-50">@{{ assignee.username }}</span>
- </span>
- </user-avatar-link>
- <span
- v-if="shouldRenderCounter"
- v-gl-tooltip
- :title="assigneeCounterTooltip"
- class="avatar-counter"
- data-placement="bottom"
- >{{ assigneeCounterLabel }}</span
- >
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue b/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue
deleted file mode 100644
index 8ddf50cb357..00000000000
--- a/app/assets/javascripts/boards/components/issue_time_estimate_deprecated.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-import { GlTooltip, GlIcon } from '@gitlab/ui';
-import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
-import boardsStore from '../stores/boards_store';
-
-export default {
- components: {
- GlIcon,
- GlTooltip,
- },
- props: {
- estimate: {
- type: [Number, String],
- required: true,
- },
- },
- data() {
- return {
- limitToHours: boardsStore.timeTracking.limitToHours,
- };
- },
- computed: {
- title() {
- return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
- },
- timeEstimate() {
- return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
- },
- },
-};
-</script>
-
-<template>
- <span>
- <span ref="issueTimeEstimate" class="board-card-info card-number">
- <gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
- timeEstimate
- }}</time>
- </span>
- <gl-tooltip
- :target="() => $refs.issueTimeEstimate"
- placement="bottom"
- class="js-issue-time-estimate"
- >
- <span class="bold d-block">{{ __('Time estimate') }}</span> {{ title }}
- </gl-tooltip>
- </span>
-</template>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
deleted file mode 100644
index 6eb1dbfb46a..00000000000
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* eslint-disable func-names, no-new */
-
-import $ from 'jquery';
-import store from '~/boards/stores';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import createFlash from '~/flash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import CreateLabelDropdown from '../../create_label';
-import { fullLabelId } from '../boards_util';
-import boardsStore from '../stores/boards_store';
-
-function shouldCreateListGraphQL(label) {
- return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
-}
-
-// eslint-disable-next-line @gitlab/no-global-event-off
-$(document)
- .off('created.label')
- .on('created.label', (e, label, addNewList) => {
- if (!addNewList) {
- return;
- }
-
- if (shouldCreateListGraphQL(label)) {
- store.dispatch('createList', { labelId: fullLabelId(label) });
- } else {
- boardsStore.new({
- title: label.title,
- position: boardsStore.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
- title: label.title,
- color: label.color,
- },
- });
- }
- });
-
-export default function initNewListDropdown() {
- $('.js-new-board-list').each(function () {
- const $dropdownToggle = $(this);
- const $dropdown = $dropdownToggle.closest('.dropdown');
- new CreateLabelDropdown(
- $dropdown.find('.dropdown-new-label'),
- $dropdownToggle.data('namespacePath'),
- $dropdownToggle.data('projectPath'),
- );
-
- initDeprecatedJQueryDropdown($dropdownToggle, {
- data(term, callback) {
- const reqFailed = () => {
- $dropdownToggle.data('bs.dropdown').hide();
- createFlash({
- message: __('Error fetching labels.'),
- });
- };
-
- if (store.getters.shouldUseGraphQL) {
- store
- .dispatch('fetchLabels')
- .then((data) => callback(data))
- .catch(reqFailed);
- } else {
- axios
- .get($dropdownToggle.attr('data-list-labels-path'))
- .then(({ data }) => callback(data))
- .catch(reqFailed);
- }
- },
- renderRow(label) {
- const active = store.getters.shouldUseGraphQL
- ? store.getters.getListByLabelId(label.id)
- : boardsStore.findListByLabelId(label.id);
- const $li = $('<li />');
- const $a = $('<a />', {
- class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
- text: label.title,
- href: '#',
- });
- const $labelColor = $('<span />', {
- class: 'dropdown-label-box',
- style: `background-color: ${label.color}`,
- });
-
- return $li.append($a.prepend($labelColor));
- },
- search: {
- fields: ['title'],
- },
- filterable: true,
- selectable: true,
- multiSelect: true,
- containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
- clicked(options) {
- const { e } = options;
- const label = options.selectedObj;
- e.preventDefault();
-
- if (shouldCreateListGraphQL(label)) {
- store.dispatch('createList', { labelId: label.id });
- } else if (!boardsStore.findListByLabelId(label.id)) {
- boardsStore.new({
- title: label.title,
- position: boardsStore.state.lists.length - 2,
- list_type: 'label',
- label: {
- id: label.id,
- title: label.title,
- color: label.color,
- },
- });
- }
- },
- });
- });
-}
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
deleted file mode 100644
index fc95ba0461d..00000000000
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ /dev/null
@@ -1,146 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { featureAccessLevel } from '~/pages/projects/shared/permissions/constants';
-import Api from '../../api';
-import { ListType } from '../constants';
-import eventHub from '../eventhub';
-
-export default {
- name: 'ProjectSelect',
- i18n: {
- headerTitle: s__(`BoardNewIssue|Projects`),
- dropdownText: s__(`BoardNewIssue|Select a project`),
- searchPlaceholder: s__(`BoardNewIssue|Search projects`),
- emptySearchResult: s__(`BoardNewIssue|No matching results`),
- },
- defaultFetchOptions: {
- with_issues_enabled: true,
- with_shared: false,
- include_subgroups: true,
- order_by: 'similarity',
- archived: false,
- },
- components: {
- GlLoadingIcon,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlSearchBoxByType,
- },
- inject: ['groupId'],
- props: {
- list: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- initialLoading: true,
- isFetching: false,
- projects: [],
- selectedProject: {},
- searchTerm: '',
- };
- },
- computed: {
- selectedProjectName() {
- return this.selectedProject.name || this.$options.i18n.dropdownText;
- },
- fetchOptions() {
- const additionalAttrs = {};
- if (this.list.type && this.list.type !== ListType.backlog) {
- additionalAttrs.min_access_level = featureAccessLevel.EVERYONE;
- }
-
- return {
- ...this.$options.defaultFetchOptions,
- ...additionalAttrs,
- };
- },
- isFetchResultEmpty() {
- return this.projects.length === 0;
- },
- },
- watch: {
- searchTerm() {
- this.fetchProjects();
- },
- },
- async mounted() {
- await this.fetchProjects();
-
- this.initialLoading = false;
- },
- methods: {
- async fetchProjects() {
- this.isFetching = true;
- try {
- const projects = await Api.groupProjects(this.groupId, this.searchTerm, this.fetchOptions);
-
- this.projects = projects.map((project) => {
- return {
- id: project.id,
- name: project.name,
- namespacedName: project.name_with_namespace,
- path: project.path_with_namespace,
- };
- });
- } catch (err) {
- /* Handled in Api.groupProjects */
- } finally {
- this.isFetching = false;
- }
- },
- selectProject(projectId) {
- this.selectedProject = this.projects.find((project) => project.id === projectId);
-
- eventHub.$emit('setSelectedProject', this.selectedProject);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <label class="gl-font-weight-bold gl-mt-3" data-testid="header-label">{{
- $options.i18n.headerTitle
- }}</label>
- <gl-dropdown
- data-testid="project-select-dropdown"
- :text="selectedProjectName"
- :header-text="$options.i18n.headerTitle"
- block
- menu-class="gl-w-full!"
- :loading="initialLoading"
- >
- <gl-search-box-by-type
- v-model.trim="searchTerm"
- debounce="250"
- :placeholder="$options.i18n.searchPlaceholder"
- />
- <gl-dropdown-item
- v-for="project in projects"
- v-show="!isFetching"
- :key="project.id"
- :name="project.name"
- @click="selectProject(project.id)"
- >
- {{ project.namespacedName }}
- </gl-dropdown-item>
- <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
- <gl-loading-icon class="gl-mx-auto" size="sm" />
- </gl-dropdown-text>
- <gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
- <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
- </gl-dropdown-text>
- </gl-dropdown>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js
index 41938d8e284..945a508c55d 100644
--- a/app/assets/javascripts/boards/config_toggle.js
+++ b/app/assets/javascripts/boards/config_toggle.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import ConfigToggle from './components/config_toggle.vue';
-export default (boardsStore = undefined) => {
+export default () => {
const el = document.querySelector('.js-board-config');
if (!el) {
@@ -15,7 +15,6 @@ export default (boardsStore = undefined) => {
render(h) {
return h(ConfigToggle, {
props: {
- boardsStore,
canAdminList: parseBoolean(el.dataset.canAdminList),
hasScope: parseBoolean(el.dataset.hasScope),
},
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 16fb4596726..391e0d1fb0a 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -119,6 +119,11 @@ export const DraggableItemTypes = {
list: 'list',
};
+export const MilestoneIDs = {
+ NONE: 0,
+ ANY: -1,
+};
+
export default {
BoardType,
ListType,
diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js
deleted file mode 100644
index 62a0d930ec0..00000000000
--- a/app/assets/javascripts/boards/ee_functions.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const setWeightFetchingState = () => {};
-export const setEpicFetchingState = () => {};
-
-export const getMilestoneTitle = () => ({});
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index c6040f1e4aa..72586970008 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -4,7 +4,6 @@ import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable
import { updateHistory } from '~/lib/utils/url_utility';
import FilteredSearchContainer from '../filtered_search/container';
import vuexstore from './stores';
-import boardsStore from './stores/boards_store';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
@@ -26,7 +25,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
this.cantEdit = cantEdit.filter((i) => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter((i) => typeof i === 'object');
- if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) {
+ if (vuexstore.state.boardConfig) {
const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig);
// TODO Refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/329274
// here we are using "window.location.search" as a temporary store
@@ -45,14 +44,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
const groupByParam = new URLSearchParams(window.location.search).get('group_by');
this.store.path = `${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`;
- if (vuexstore.getters.shouldUseGraphQL) {
- updateHistory({
- url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
- });
- vuexstore.dispatch('performSearch');
- } else if (this.updateUrl) {
- boardsStore.updateFiltersUrl();
- }
+ updateHistory({
+ url: `?${path.substr(1)}${groupByParam ? `&group_by=${groupByParam}` : ''}`,
+ });
+ vuexstore.dispatch('performSearch');
}
removeTokens() {
diff --git a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
new file mode 100644
index 00000000000..1c382c4747b
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
@@ -0,0 +1,10 @@
+query GroupBoardIterations($fullPath: ID!, $title: String) {
+ group(fullPath: $fullPath) {
+ iterations(includeAncestors: true, title: $title) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
index 0ff70703e1a..1b14396fb5c 100644
--- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
@@ -12,6 +12,7 @@ fragment IssueNode on Issue {
humanTotalTimeSpent
emailsDisabled
confidential
+ hidden
webUrl
relativePosition
assignees {
diff --git a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
new file mode 100644
index 00000000000..078151a275a
--- /dev/null
+++ b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
@@ -0,0 +1,10 @@
+query ProjectBoardIterations($fullPath: ID!, $title: String) {
+ project(fullPath: $fullPath) {
+ iterations(includeAncestors: true, title: $title) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
index 776530ebb83..724b7f5a34c 100644
--- a/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_milestones.query.graphql
@@ -1,4 +1,4 @@
-query groupMilestones(
+query projectMilestones(
$fullPath: ID!
$state: MilestoneStateEnum
$includeAncestors: Boolean
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index de7c8a3bd6b..21c1bb23dc6 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -2,41 +2,20 @@ import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import PortalVue from 'portal-vue';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { mapActions, mapGetters } from 'vuex';
-import 'ee_else_ce/boards/models/issue';
-import 'ee_else_ce/boards/models/list';
-import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
-import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
-import {
- setWeightFetchingState,
- setEpicFetchingState,
- getMilestoneTitle,
-} from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
-import BoardContent from '~/boards/components/board_content.vue';
-import './models/label';
-import './models/assignee';
-import '~/boards/models/milestone';
-import '~/boards/models/project';
+import BoardApp from '~/boards/components/board_app.vue';
import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus';
import createDefaultClient from '~/lib/graphql';
-import {
- NavigationType,
- convertObjectPropsToCamelCase,
- parseBoolean,
-} from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import sidebarEventHub from '~/sidebar/event_hub';
+import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
@@ -61,10 +40,75 @@ const apolloProvider = new VueApollo({
),
});
-let issueBoardsApp;
+function mountBoardApp(el) {
+ const { boardId, groupId, fullPath, rootPath } = el.dataset;
+
+ store.dispatch('setInitialBoardData', {
+ boardId,
+ fullBoardId: fullBoardId(boardId),
+ fullPath,
+ boardType: el.dataset.parent,
+ disabled: parseBoolean(el.dataset.disabled) || true,
+ issuableType: issuableTypes.issue,
+ boardConfig: {
+ milestoneId: parseInt(el.dataset.boardMilestoneId, 10),
+ milestoneTitle: el.dataset.boardMilestoneTitle || '',
+ iterationId: parseInt(el.dataset.boardIterationId, 10),
+ iterationTitle: el.dataset.boardIterationTitle || '',
+ assigneeId: el.dataset.boardAssigneeId,
+ assigneeUsername: el.dataset.boardAssigneeUsername,
+ labels: el.dataset.labels ? JSON.parse(el.dataset.labels) : [],
+ labelIds: el.dataset.labelIds ? JSON.parse(el.dataset.labelIds) : [],
+ weight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
+ },
+ });
+
+ if (!gon?.features?.issueBoardsFilteredSearch) {
+ // Warning: FilteredSearchBoards has an implicit dependency on the Vuex state 'boardConfig'
+ // Improve this situation in the future.
+ const filterManager = new FilteredSearchBoards({ path: '' }, true, []);
+ filterManager.setup();
+
+ eventHub.$on('updateTokens', () => {
+ filterManager.updateTokens();
+ });
+ }
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ store,
+ apolloProvider,
+ provide: {
+ disabled: parseBoolean(el.dataset.disabled),
+ boardId,
+ groupId: Number(groupId),
+ rootPath,
+ currentUserId: gon.current_user_id || null,
+ canUpdate: parseBoolean(el.dataset.canUpdate),
+ canAdminList: parseBoolean(el.dataset.canAdminList),
+ labelsManagePath: el.dataset.labelsManagePath,
+ labelsFilterBasePath: el.dataset.labelsFilterBasePath,
+ timeTrackingLimitToHours: parseBoolean(el.dataset.timeTrackingLimitToHours),
+ multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable),
+ epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable),
+ iterationFeatureAvailable: parseBoolean(el.dataset.iterationFeatureAvailable),
+ weightFeatureAvailable: parseBoolean(el.dataset.weightFeatureAvailable),
+ boardWeight: el.dataset.boardWeight ? parseInt(el.dataset.boardWeight, 10) : null,
+ scopedLabelsAvailable: parseBoolean(el.dataset.scopedLabels),
+ milestoneListsAvailable: parseBoolean(el.dataset.milestoneListsAvailable),
+ assigneeListsAvailable: parseBoolean(el.dataset.assigneeListsAvailable),
+ iterationListsAvailable: parseBoolean(el.dataset.iterationListsAvailable),
+ issuableType: issuableTypes.issue,
+ emailsDisabled: parseBoolean(el.dataset.emailsDisabled),
+ },
+ render: (createComponent) => createComponent(BoardApp),
+ });
+}
export default () => {
- const $boardApp = document.getElementById('board-app');
+ const $boardApp = document.getElementById('js-issuable-board-app');
+
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward =
@@ -75,257 +119,11 @@ export default () => {
}
});
- if (issueBoardsApp) {
- issueBoardsApp.$destroy(true);
- }
-
if (gon?.features?.issueBoardsFilteredSearch) {
initBoardsFilteredSearch(apolloProvider);
}
- if (!gon?.features?.graphqlBoardLists) {
- boardsStore.create();
- boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
- }
-
- // eslint-disable-next-line @gitlab/no-runtime-template-compiler
- issueBoardsApp = new Vue({
- el: $boardApp,
- components: {
- BoardContent,
- BoardSidebar,
- BoardSettingsSidebar: () => import('~/boards/components/board_settings_sidebar.vue'),
- },
- provide: {
- boardId: $boardApp.dataset.boardId,
- groupId: Number($boardApp.dataset.groupId),
- rootPath: $boardApp.dataset.rootPath,
- currentUserId: gon.current_user_id || null,
- canUpdate: parseBoolean($boardApp.dataset.canUpdate),
- canAdminList: parseBoolean($boardApp.dataset.canAdminList),
- labelsManagePath: $boardApp.dataset.labelsManagePath,
- labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath,
- timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours),
- multipleAssigneesFeatureAvailable: parseBoolean(
- $boardApp.dataset.multipleAssigneesFeatureAvailable,
- ),
- epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable),
- iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable),
- weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable),
- boardWeight: $boardApp.dataset.boardWeight
- ? parseInt($boardApp.dataset.boardWeight, 10)
- : null,
- scopedLabelsAvailable: parseBoolean($boardApp.dataset.scopedLabels),
- milestoneListsAvailable: parseBoolean($boardApp.dataset.milestoneListsAvailable),
- assigneeListsAvailable: parseBoolean($boardApp.dataset.assigneeListsAvailable),
- iterationListsAvailable: parseBoolean($boardApp.dataset.iterationListsAvailable),
- issuableType: issuableTypes.issue,
- emailsDisabled: parseBoolean($boardApp.dataset.emailsDisabled),
- },
- store,
- apolloProvider,
- data() {
- return {
- state: boardsStore.state,
- loading: 0,
- boardsEndpoint: $boardApp.dataset.boardsEndpoint,
- recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
- listsEndpoint: $boardApp.dataset.listsEndpoint,
- disabled: parseBoolean($boardApp.dataset.disabled),
- bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
- detailIssue: boardsStore.detail,
- parent: $boardApp.dataset.parent,
- };
- },
- computed: {
- ...mapGetters(['shouldUseGraphQL']),
- detailIssueVisible() {
- return Object.keys(this.detailIssue.issue).length;
- },
- },
- created() {
- this.setInitialBoardData({
- boardId: $boardApp.dataset.boardId,
- fullBoardId: fullBoardId($boardApp.dataset.boardId),
- fullPath: $boardApp.dataset.fullPath,
- boardType: this.parent,
- disabled: this.disabled,
- issuableType: issuableTypes.issue,
- boardConfig: {
- milestoneId: parseInt($boardApp.dataset.boardMilestoneId, 10),
- milestoneTitle: $boardApp.dataset.boardMilestoneTitle || '',
- iterationId: parseInt($boardApp.dataset.boardIterationId, 10),
- iterationTitle: $boardApp.dataset.boardIterationTitle || '',
- assigneeId: $boardApp.dataset.boardAssigneeId,
- assigneeUsername: $boardApp.dataset.boardAssigneeUsername,
- labels: $boardApp.dataset.labels ? JSON.parse($boardApp.dataset.labels) : [],
- labelIds: $boardApp.dataset.labelIds ? JSON.parse($boardApp.dataset.labelIds) : [],
- weight: $boardApp.dataset.boardWeight
- ? parseInt($boardApp.dataset.boardWeight, 10)
- : null,
- },
- });
- boardsStore.setEndpoints({
- boardsEndpoint: this.boardsEndpoint,
- recentBoardsEndpoint: this.recentBoardsEndpoint,
- listsEndpoint: this.listsEndpoint,
- bulkUpdatePath: this.bulkUpdatePath,
- boardId: $boardApp.dataset.boardId,
- fullPath: $boardApp.dataset.fullPath,
- });
- boardsStore.rootPath = this.boardsEndpoint;
-
- eventHub.$on('updateTokens', this.updateTokens);
- eventHub.$on('newDetailIssue', this.updateDetailIssue);
- eventHub.$on('clearDetailIssue', this.clearDetailIssue);
- sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
- eventHub.$on('initialBoardLoad', this.initialBoardLoad);
- },
- beforeDestroy() {
- eventHub.$off('updateTokens', this.updateTokens);
- eventHub.$off('newDetailIssue', this.updateDetailIssue);
- eventHub.$off('clearDetailIssue', this.clearDetailIssue);
- sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
- eventHub.$off('initialBoardLoad', this.initialBoardLoad);
- },
- mounted() {
- if (!gon?.features?.issueBoardsFilteredSearch) {
- this.filterManager = new FilteredSearchBoards(
- boardsStore.filter,
- true,
- boardsStore.cantEdit,
- );
- this.filterManager.setup();
- }
-
- this.performSearch();
-
- boardsStore.disabled = this.disabled;
-
- if (!this.shouldUseGraphQL) {
- this.initialBoardLoad();
- }
- },
- methods: {
- ...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
- initialBoardLoad() {
- boardsStore
- .all()
- .then((res) => res.data)
- .then((lists) => {
- lists.forEach((list) => boardsStore.addList(list));
- this.loading = false;
- })
- .catch((error) => {
- this.setError({
- error,
- message: __('An error occurred while fetching the board lists. Please try again.'),
- });
- });
- },
- updateTokens() {
- this.filterManager.updateTokens();
- },
- updateDetailIssue(newIssue, multiSelect = false) {
- const { sidebarInfoEndpoint } = newIssue;
- if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
- newIssue.setFetchingState('subscriptions', true);
- setWeightFetchingState(newIssue, true);
- setEpicFetchingState(newIssue, true);
- boardsStore
- .getIssueInfo(sidebarInfoEndpoint)
- .then((res) => res.data)
- .then((data) => {
- const {
- subscribed,
- totalTimeSpent,
- timeEstimate,
- humanTimeEstimate,
- humanTotalTimeSpent,
- weight,
- epic,
- assignees,
- } = convertObjectPropsToCamelCase(data);
-
- newIssue.setFetchingState('subscriptions', false);
- setWeightFetchingState(newIssue, false);
- setEpicFetchingState(newIssue, false);
- newIssue.updateData({
- humanTimeSpent: humanTotalTimeSpent,
- timeSpent: totalTimeSpent,
- humanTimeEstimate,
- timeEstimate,
- subscribed,
- weight,
- epic,
- assignees,
- });
- })
- .catch(() => {
- newIssue.setFetchingState('subscriptions', false);
- setWeightFetchingState(newIssue, false);
- this.setError({ message: __('An error occurred while fetching sidebar data') });
- });
- }
-
- if (multiSelect) {
- boardsStore.toggleMultiSelect(newIssue);
-
- if (boardsStore.detail.issue) {
- boardsStore.clearDetailIssue();
- return;
- }
-
- return;
- }
-
- boardsStore.setIssueDetail(newIssue);
- },
- clearDetailIssue(multiSelect = false) {
- if (multiSelect) {
- boardsStore.clearMultiSelect();
- }
- boardsStore.clearDetailIssue();
- },
- toggleSubscription(id) {
- const { issue } = boardsStore.detail;
- if (issue.id === id && issue.toggleSubscriptionEndpoint) {
- issue.setFetchingState('subscriptions', true);
- boardsStore
- .toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
- .then(() => {
- issue.setFetchingState('subscriptions', false);
- issue.updateData({
- subscribed: !issue.subscribed,
- });
- })
- .catch(() => {
- issue.setFetchingState('subscriptions', false);
- this.setError({
- message: __('An error occurred when toggling the notification subscription'),
- });
- });
- }
- },
- getNodes(data) {
- return data[this.parent]?.board?.lists.nodes;
- },
- },
- });
-
- // eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
- new Vue({
- el: document.getElementById('js-add-list'),
- data() {
- return {
- filters: boardsStore.state.filters,
- ...getMilestoneTitle($boardApp),
- };
- },
- mounted() {
- initNewListDropdown();
- },
- });
+ mountBoardApp($boardApp);
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
@@ -342,7 +140,7 @@ export default () => {
});
}
- boardConfigToggle(boardsStore);
+ boardConfigToggle();
toggleFocusMode();
toggleLabels();
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
deleted file mode 100644
index 1e822d06bfd..00000000000
--- a/app/assets/javascripts/boards/models/assignee.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default class ListAssignee {
- constructor(obj) {
- this.id = obj.id;
- this.name = obj.name;
- this.username = obj.username;
- this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url;
- this.path = obj.path;
- this.state = obj.state;
- this.webUrl = obj.web_url || obj.webUrl;
- }
-}
-
-window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
deleted file mode 100644
index 46d1239457d..00000000000
--- a/app/assets/javascripts/boards/models/issue.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* eslint-disable no-unused-vars */
-/* global ListLabel */
-/* global ListMilestone */
-/* global ListAssignee */
-
-import axios from '~/lib/utils/axios_utils';
-import './label';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import boardsStore from '../stores/boards_store';
-import IssueProject from './project';
-
-class ListIssue {
- constructor(obj) {
- this.subscribed = obj.subscribed;
- this.labels = [];
- this.assignees = [];
- this.selected = false;
- this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity;
- this.isFetching = {
- subscriptions: true,
- };
- this.closed = obj.closed;
- this.isLoading = {};
-
- this.refreshData(obj);
- }
-
- refreshData(obj) {
- boardsStore.refreshIssueData(this, obj);
- }
-
- addLabel(label) {
- boardsStore.addIssueLabel(this, label);
- }
-
- findLabel(findLabel) {
- return boardsStore.findIssueLabel(this, findLabel);
- }
-
- removeLabel(removeLabel) {
- boardsStore.removeIssueLabel(this, removeLabel);
- }
-
- removeLabels(labels) {
- boardsStore.removeIssueLabels(this, labels);
- }
-
- addAssignee(assignee) {
- boardsStore.addIssueAssignee(this, assignee);
- }
-
- findAssignee(findAssignee) {
- return boardsStore.findIssueAssignee(this, findAssignee);
- }
-
- setAssignees(assignees) {
- boardsStore.setIssueAssignees(this, assignees);
- }
-
- removeAssignee(removeAssignee) {
- boardsStore.removeIssueAssignee(this, removeAssignee);
- }
-
- removeAllAssignees() {
- boardsStore.removeAllIssueAssignees(this);
- }
-
- addMilestone(milestone) {
- boardsStore.addIssueMilestone(this, milestone);
- }
-
- removeMilestone(removeMilestone) {
- boardsStore.removeIssueMilestone(this, removeMilestone);
- }
-
- getLists() {
- return boardsStore.state.lists.filter((list) => list.findIssue(this.id));
- }
-
- updateData(newData) {
- boardsStore.updateIssueData(this, newData);
- }
-
- setFetchingState(key, value) {
- boardsStore.setIssueFetchingState(this, key, value);
- }
-
- setLoadingState(key, value) {
- boardsStore.setIssueLoadingState(this, key, value);
- }
-
- update() {
- return boardsStore.updateIssue(this);
- }
-}
-
-window.ListIssue = ListIssue;
-
-export default ListIssue;
diff --git a/app/assets/javascripts/boards/models/iteration.js b/app/assets/javascripts/boards/models/iteration.js
deleted file mode 100644
index b7bdc204f7c..00000000000
--- a/app/assets/javascripts/boards/models/iteration.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default class ListIteration {
- constructor(obj) {
- this.id = obj.id;
- this.title = obj.title;
- this.state = obj.state;
- this.webUrl = obj.web_url || obj.webUrl;
- this.description = obj.description;
- }
-}
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js
deleted file mode 100644
index cd2a2c0137f..00000000000
--- a/app/assets/javascripts/boards/models/label.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-
-export default class ListLabel {
- constructor(obj) {
- Object.assign(this, convertObjectPropsToCamelCase(obj, { dropKeys: ['priority'] }), {
- priority: obj.priority !== null ? obj.priority : Infinity,
- });
- }
-}
-
-window.ListLabel = ListLabel;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
deleted file mode 100644
index ab24532d87f..00000000000
--- a/app/assets/javascripts/boards/models/list.js
+++ /dev/null
@@ -1,182 +0,0 @@
-/* eslint-disable class-methods-use-this */
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import boardsStore from '../stores/boards_store';
-import ListAssignee from './assignee';
-import ListIteration from './iteration';
-import ListLabel from './label';
-import ListMilestone from './milestone';
-import 'ee_else_ce/boards/models/issue';
-
-const TYPES = {
- backlog: {
- isPreset: true,
- isExpandable: true,
- isBlank: false,
- },
- closed: {
- isPreset: true,
- isExpandable: true,
- isBlank: false,
- },
- blank: {
- isPreset: true,
- isExpandable: false,
- isBlank: true,
- },
- default: {
- // includes label, assignee, and milestone lists
- isPreset: false,
- isExpandable: true,
- isBlank: false,
- },
-};
-
-class List {
- constructor(obj) {
- this.id = obj.id;
- this.position = obj.position;
- this.title = obj.title;
- this.type = obj.list_type || obj.listType;
-
- const typeInfo = this.getTypeInfo(this.type);
- this.preset = Boolean(typeInfo.isPreset);
- this.isExpandable = Boolean(typeInfo.isExpandable);
- this.isExpanded = !obj.collapsed;
- this.page = 1;
- this.highlighted = obj.highlighted;
- this.loading = true;
- this.loadingMore = false;
- this.issues = obj.issues || [];
- this.issuesSize = obj.issuesSize || obj.issuesCount || 0;
- this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
-
- if (obj.label) {
- this.label = new ListLabel(obj.label);
- } else if (obj.user || obj.assignee) {
- this.assignee = new ListAssignee(obj.user || obj.assignee);
- this.title = this.assignee.name;
- } else if (IS_EE && obj.milestone) {
- this.milestone = new ListMilestone(obj.milestone);
- this.title = this.milestone.title;
- } else if (IS_EE && obj.iteration) {
- this.iteration = new ListIteration(obj.iteration);
- this.title = this.iteration.title;
- }
-
- // doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
- // Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/229416
- if (!typeInfo.isBlank && this.id && !obj.doNotFetchIssues) {
- this.getIssues().catch(() => {
- // TODO: handle request error
- });
- }
- }
-
- guid() {
- const s4 = () =>
- Math.floor((1 + Math.random()) * 0x10000)
- .toString(16)
- .substring(1);
- return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
- }
-
- save() {
- return boardsStore.saveList(this);
- }
-
- destroy() {
- boardsStore.destroy(this);
- }
-
- update() {
- return boardsStore.updateListFunc(this);
- }
-
- nextPage() {
- return boardsStore.goToNextPage(this);
- }
-
- getIssues(emptyIssues = true) {
- return boardsStore.getListIssues(this, emptyIssues);
- }
-
- newIssue(issue) {
- return boardsStore.newListIssue(this, issue);
- }
-
- addMultipleIssues(issues, listFrom, newIndex) {
- boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex);
- }
-
- addIssue(issue, listFrom, newIndex) {
- boardsStore.addListIssue(this, issue, listFrom, newIndex);
- }
-
- moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
- boardsStore.moveListIssues(this, issue, oldIndex, newIndex, moveBeforeId, moveAfterId);
- }
-
- moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
- boardsStore
- .moveListMultipleIssues({
- list: this,
- issues,
- oldIndicies,
- newIndex,
- moveBeforeId,
- moveAfterId,
- })
- .catch(() =>
- createFlash({
- message: __('Something went wrong while moving issues.'),
- }),
- );
- }
-
- updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- boardsStore.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId).catch(() => {
- // TODO: handle request error
- });
- }
-
- updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) {
- boardsStore
- .moveMultipleIssues({
- ids: issues.map((issue) => issue.id),
- fromListId: listFrom.id,
- toListId: this.id,
- moveBeforeId,
- moveAfterId,
- })
- .catch(() =>
- createFlash({
- message: __('Something went wrong while moving issues.'),
- }),
- );
- }
-
- findIssue(id) {
- return boardsStore.findListIssue(this, id);
- }
-
- removeMultipleIssues(removeIssues) {
- return boardsStore.removeListMultipleIssues(this, removeIssues);
- }
-
- removeIssue(removeIssue) {
- return boardsStore.removeListIssues(this, removeIssue);
- }
-
- getTypeInfo(type) {
- return TYPES[type] || TYPES.default;
- }
-
- onNewIssueResponse(issue, data) {
- boardsStore.onNewListIssueResponse(this, issue, data);
- }
-}
-
-window.List = List;
-
-export default List;
diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js
deleted file mode 100644
index 7201b6e91f5..00000000000
--- a/app/assets/javascripts/boards/models/milestone.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default class ListMilestone {
- constructor(obj) {
- this.id = obj.id;
- this.title = obj.title;
-
- if (IS_EE) {
- this.path = obj.path;
- this.state = obj.state;
- this.webUrl = obj.web_url || obj.webUrl;
- this.description = obj.description;
- }
- }
-}
-
-window.ListMilestone = ListMilestone;
diff --git a/app/assets/javascripts/boards/models/project.js b/app/assets/javascripts/boards/models/project.js
deleted file mode 100644
index 9468a02856e..00000000000
--- a/app/assets/javascripts/boards/models/project.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export default class IssueProject {
- constructor(obj) {
- this.id = obj.id;
- this.path = obj.path;
- this.fullPath = obj.path_with_namespace;
- }
-}
diff --git a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
index 7d6179a8547..a3a8ad06c43 100644
--- a/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
+++ b/app/assets/javascripts/boards/mount_multiple_boards_switcher.js
@@ -1,12 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { mapGetters } from 'vuex';
import BoardsSelector from 'ee_else_ce/boards/components/boards_selector.vue';
-import BoardsSelectorDeprecated from '~/boards/components/boards_selector_deprecated.vue';
import store from '~/boards/stores';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
Vue.use(VueApollo);
@@ -25,9 +22,7 @@ export default (params = {}) => {
el: boardsSwitcherElement,
components: {
BoardsSelector,
- BoardsSelectorDeprecated,
},
- mixins: [glFeatureFlagMixin()],
apolloProvider,
store,
provide: {
@@ -52,16 +47,8 @@ export default (params = {}) => {
return { boardsSelectorProps };
},
- computed: {
- ...mapGetters(['shouldUseGraphQL', 'isEpicBoard']),
- },
render(createElement) {
- if (this.shouldUseGraphQL || this.isEpicBoard) {
- return createElement(BoardsSelector, {
- props: this.boardsSelectorProps,
- });
- }
- return createElement(BoardsSelectorDeprecated, {
+ return createElement(BoardsSelector, {
props: this.boardsSelectorProps,
});
},
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 970d00841bd..dc06b62cebb 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -36,11 +36,13 @@ import {
filterVariables,
} from '../boards_util';
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
+import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
+import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import * as types from './mutation_types';
@@ -82,11 +84,8 @@ export default {
'setFilters',
convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
);
-
- if (gon.features.graphqlBoardLists) {
- dispatch('fetchLists');
- dispatch('resetIssues');
- }
+ dispatch('fetchLists');
+ dispatch('resetIssues');
},
fetchLists: ({ commit, state, dispatch }) => {
@@ -182,7 +181,7 @@ export default {
});
},
- fetchLabels: ({ state, commit, getters }, searchTerm) => {
+ fetchLabels: ({ state, commit }, searchTerm) => {
const { fullPath, boardType } = state;
const variables = {
@@ -200,14 +199,7 @@ export default {
variables,
})
.then(({ data }) => {
- let labels = data[boardType]?.labels.nodes;
-
- if (!getters.shouldUseGraphQL && !getters.isEpicBoard) {
- labels = labels.map((label) => ({
- ...label,
- id: getIdFromGraphQLId(label.id),
- }));
- }
+ const labels = data[boardType]?.labels.nodes;
commit(types.RECEIVE_LABELS_SUCCESS, labels);
return labels;
@@ -218,6 +210,52 @@ export default {
});
},
+ fetchIterations({ state, commit }, title) {
+ commit(types.RECEIVE_ITERATIONS_REQUEST);
+
+ const { fullPath, boardType } = state;
+
+ const variables = {
+ fullPath,
+ title,
+ };
+
+ let query;
+ if (boardType === BoardType.project) {
+ query = projectBoardIterationsQuery;
+ }
+ if (boardType === BoardType.group) {
+ query = groupBoardIterationsQuery;
+ }
+
+ if (!query) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Unknown board type');
+ }
+
+ return gqlClient
+ .query({
+ query,
+ variables,
+ })
+ .then(({ data }) => {
+ const errors = data[boardType]?.errors;
+ const iterations = data[boardType]?.iterations.nodes;
+
+ if (errors?.[0]) {
+ throw new Error(errors[0]);
+ }
+
+ commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
+
+ return iterations;
+ })
+ .catch((e) => {
+ commit(types.RECEIVE_ITERATIONS_FAILURE);
+ throw e;
+ });
+ },
+
fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST);
@@ -536,8 +574,8 @@ export default {
boardId: fullBoardId,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
- moveBeforeId,
- moveAfterId,
+ moveBeforeId: moveBeforeId ? getIdFromGraphQLId(moveBeforeId) : undefined,
+ moveAfterId: moveAfterId ? getIdFromGraphQLId(moveAfterId) : undefined,
// 'mutationVariables' allows EE code to pass in extra parameters.
...mutationVariables,
},
@@ -604,7 +642,7 @@ export default {
}
const rawIssue = data.createIssue?.issue;
- const formattedIssue = formatIssue({ ...rawIssue, id: getIdFromGraphQLId(rawIssue.id) });
+ const formattedIssue = formatIssue(rawIssue);
dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
dispatch('addListItem', { list, item: formattedIssue, position: 0 });
})
@@ -640,7 +678,7 @@ export default {
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: getIdFromGraphQLId(data.updateIssue?.issue?.id) || activeBoardItem.id,
+ itemId: data.updateIssue?.issue?.id || activeBoardItem.id,
prop: 'labels',
value: data.updateIssue.issue.labels.nodes,
});
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
deleted file mode 100644
index 857b0912c57..00000000000
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ /dev/null
@@ -1,883 +0,0 @@
-/* eslint-disable no-shadow, no-param-reassign,consistent-return */
-/* global List */
-/* global ListIssue */
-import { sortBy } from 'lodash';
-import Vue from 'vue';
-import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import createDefaultClient from '~/lib/graphql';
-import axios from '~/lib/utils/axios_utils';
-import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { mergeUrlParams, queryToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
-import { ListType, flashAnimationDuration } from '../constants';
-import eventHub from '../eventhub';
-import ListAssignee from '../models/assignee';
-import ListLabel from '../models/label';
-import ListMilestone from '../models/milestone';
-import IssueProject from '../models/project';
-
-const PER_PAGE = 20;
-export const gqlClient = createDefaultClient();
-
-const boardsStore = {
- disabled: false,
- timeTracking: {
- limitToHours: false,
- },
- scopedLabels: {
- enabled: false,
- },
- filter: {
- path: '',
- },
- state: {
- currentBoard: {
- labels: [],
- },
- currentPage: '',
- endpoints: {},
- },
- detail: {
- issue: {},
- list: {},
- },
- moving: {
- issue: {},
- list: {},
- },
- multiSelect: { list: [] },
-
- setEndpoints({
- boardsEndpoint,
- listsEndpoint,
- bulkUpdatePath,
- boardId,
- recentBoardsEndpoint,
- fullPath,
- }) {
- const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
- this.state.endpoints = {
- boardsEndpoint,
- boardId,
- listsEndpoint,
- listsEndpointGenerate,
- bulkUpdatePath,
- fullPath,
- recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
- };
- },
- create() {
- this.state.lists = [];
- this.filter.path = getUrlParamsArray().join('&');
- this.detail = {
- issue: {},
- list: {},
- };
- },
- showPage(page) {
- this.state.currentPage = page;
- },
- updateListPosition(listObj) {
- const listType = listObj.listType || listObj.list_type;
- let { position } = listObj;
- if (listType === ListType.closed) {
- position = Infinity;
- } else if (listType === ListType.backlog) {
- position = -1;
- }
-
- const list = new List({ ...listObj, position });
- return list;
- },
- addList(listObj) {
- const list = this.updateListPosition(listObj);
- this.state.lists = sortBy([...this.state.lists, list], 'position');
- return list;
- },
- new(listObj) {
- const list = this.addList(listObj);
- const backlogList = this.findList('type', 'backlog');
-
- list
- .save()
- .then(() => {
- list.highlighted = true;
- setTimeout(() => {
- list.highlighted = false;
- }, flashAnimationDuration);
-
- // Remove any new issues from the backlog
- // as they will be visible in the new list
- list.issues.forEach(backlogList.removeIssue.bind(backlogList));
- this.state.lists = sortBy(this.state.lists, 'position');
- })
- .catch(() => {
- // https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
- });
- },
-
- updateNewListDropdown(listId) {
- document
- .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
- ?.classList.remove('is-active');
- },
-
- findIssueLabel(issue, findLabel) {
- return issue.labels.find((label) => label.id === findLabel.id);
- },
-
- goToNextPage(list) {
- if (list.issuesSize > list.issues.length) {
- if (list.issues.length / PER_PAGE >= 1) {
- list.page += 1;
- }
-
- return list.getIssues(false);
- }
- },
-
- addListIssue(list, issue, listFrom, newIndex) {
- let moveBeforeId = null;
- let moveAfterId = null;
-
- if (!list.findIssue(issue.id)) {
- if (newIndex !== undefined) {
- list.issues.splice(newIndex, 0, issue);
-
- if (list.issues[newIndex - 1]) {
- moveBeforeId = list.issues[newIndex - 1].id;
- }
-
- if (list.issues[newIndex + 1]) {
- moveAfterId = list.issues[newIndex + 1].id;
- }
- } else {
- list.issues.push(issue);
- }
-
- if (list.label) {
- issue.addLabel(list.label);
- }
-
- if (list.assignee) {
- if (listFrom && listFrom.type === 'assignee') {
- issue.removeAssignee(listFrom.assignee);
- }
- issue.addAssignee(list.assignee);
- }
-
- if (IS_EE && list.milestone) {
- if (listFrom && listFrom.type === 'milestone') {
- issue.removeMilestone(listFrom.milestone);
- }
- issue.addMilestone(list.milestone);
- }
-
- if (listFrom) {
- list.issuesSize += 1;
-
- list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
- }
- }
- },
- findListIssue(list, id) {
- return list.issues.find((issue) => issue.id === id);
- },
-
- removeList(id) {
- const list = this.findList('id', id);
-
- if (!list) return;
-
- this.state.lists = this.state.lists.filter((list) => list.id !== id);
- },
- moveList(listFrom, orderLists) {
- orderLists.forEach((id, i) => {
- const list = this.findList('id', parseInt(id, 10));
-
- list.position = i;
- });
- listFrom.update();
- },
-
- addMultipleListIssues(list, issues, listFrom, newIndex) {
- let moveBeforeId = null;
- let moveAfterId = null;
-
- const listHasIssues = issues.every((issue) => list.findIssue(issue.id));
-
- if (!listHasIssues) {
- if (newIndex !== undefined) {
- if (list.issues[newIndex - 1]) {
- moveBeforeId = list.issues[newIndex - 1].id;
- }
-
- if (list.issues[newIndex]) {
- moveAfterId = list.issues[newIndex].id;
- }
-
- list.issues.splice(newIndex, 0, ...issues);
- } else {
- list.issues.push(...issues);
- }
-
- if (list.label) {
- issues.forEach((issue) => issue.addLabel(list.label));
- }
-
- if (list.assignee) {
- if (listFrom && listFrom.type === 'assignee') {
- issues.forEach((issue) => issue.removeAssignee(listFrom.assignee));
- }
- issues.forEach((issue) => issue.addAssignee(list.assignee));
- }
-
- if (IS_EE && list.milestone) {
- if (listFrom && listFrom.type === 'milestone') {
- issues.forEach((issue) => issue.removeMilestone(listFrom.milestone));
- }
- issues.forEach((issue) => issue.addMilestone(list.milestone));
- }
-
- if (listFrom) {
- list.issuesSize += issues.length;
-
- list.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
- }
- }
- },
-
- removeListIssues(list, removeIssue) {
- list.issues = list.issues.filter((issue) => {
- const matchesRemove = removeIssue.id === issue.id;
-
- if (matchesRemove) {
- list.issuesSize -= 1;
- issue.removeLabel(list.label);
- }
-
- return !matchesRemove;
- });
- },
- removeListMultipleIssues(list, removeIssues) {
- const ids = removeIssues.map((issue) => issue.id);
-
- list.issues = list.issues.filter((issue) => {
- const matchesRemove = ids.includes(issue.id);
-
- if (matchesRemove) {
- list.issuesSize -= 1;
- issue.removeLabel(list.label);
- }
-
- return !matchesRemove;
- });
- },
-
- startMoving(list, issue) {
- Object.assign(this.moving, { list, issue });
- },
-
- onNewListIssueResponse(list, issue, data) {
- issue.refreshData(data);
-
- if (list.issues.length > 1) {
- const moveBeforeId = list.issues[1].id;
- this.moveIssue(issue.id, null, null, null, moveBeforeId);
- }
- },
-
- moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
- const issueTo = issues.map((issue) => listTo.findIssue(issue.id));
- const issueLists = issues.map((issue) => issue.getLists()).flat();
- const listLabels = issueLists.map((list) => list.label);
- const hasMoveableIssues = issueTo.filter(Boolean).length > 0;
-
- if (!hasMoveableIssues) {
- // Check if target list assignee is already present in this issue
- if (
- listTo.type === ListType.assignee &&
- listFrom.type === ListType.assignee &&
- issues.some((issue) => issue.findAssignee(listTo.assignee))
- ) {
- const targetIssues = issues.map((issue) => listTo.findIssue(issue.id));
- targetIssues.forEach((targetIssue) => targetIssue.removeAssignee(listFrom.assignee));
- } else if (listTo.type === 'milestone') {
- const currentMilestones = issues.map((issue) => issue.milestone);
- const currentLists = this.state.lists
- .filter((list) => list.type === 'milestone' && list.id !== listTo.id)
- .filter((list) =>
- list.issues.some((listIssue) => issues.some((issue) => listIssue.id === issue.id)),
- );
-
- issues.forEach((issue) => {
- currentMilestones.forEach((milestone) => {
- issue.removeMilestone(milestone);
- });
- });
-
- issues.forEach((issue) => {
- issue.addMilestone(listTo.milestone);
- });
-
- currentLists.forEach((currentList) => {
- issues.forEach((issue) => {
- currentList.removeIssue(issue);
- });
- });
-
- listTo.addMultipleIssues(issues, listFrom, newIndex);
- } else {
- // Add to new lists issues if it doesn't already exist
- listTo.addMultipleIssues(issues, listFrom, newIndex);
- }
- } else {
- listTo.updateMultipleIssues(issues, listFrom);
- issues.forEach((issue) => {
- issue.removeLabel(listFrom.label);
- });
- }
-
- if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) {
- issueLists.forEach((list) => {
- issues.forEach((issue) => {
- list.removeIssue(issue);
- });
- });
-
- issues.forEach((issue) => {
- issue.removeLabels(listLabels);
- });
- } else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) {
- issues.forEach((issue) => {
- issue.removeAssignee(listFrom.assignee);
- });
- issueLists.forEach((list) => {
- issues.forEach((issue) => {
- list.removeIssue(issue);
- });
- });
- } else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) {
- issues.forEach((issue) => {
- issue.removeMilestone(listFrom.milestone);
- });
- issueLists.forEach((list) => {
- issues.forEach((issue) => {
- list.removeIssue(issue);
- });
- });
- } else if (
- this.shouldRemoveIssue(listFrom, listTo) &&
- this.issuesAreContiguous(listFrom, issues)
- ) {
- listFrom.removeMultipleIssues(issues);
- }
- },
-
- issuesAreContiguous(list, issues) {
- // When there's only 1 issue selected, we can return early.
- if (issues.length === 1) return true;
-
- // Create list of ids for issues involved.
- const listIssueIds = list.issues.map((issue) => issue.id);
- const movedIssueIds = issues.map((issue) => issue.id);
-
- // Check if moved issue IDs is sub-array
- // of source list issue IDs (i.e. contiguous selection).
- return listIssueIds.join('|').includes(movedIssueIds.join('|'));
- },
-
- moveIssueToList(listFrom, listTo, issue, newIndex) {
- const issueTo = listTo.findIssue(issue.id);
- const issueLists = issue.getLists();
- const listLabels = issueLists.map((listIssue) => listIssue.label);
-
- if (!issueTo) {
- // Check if target list assignee is already present in this issue
- if (
- listTo.type === 'assignee' &&
- listFrom.type === 'assignee' &&
- issue.findAssignee(listTo.assignee)
- ) {
- const targetIssue = listTo.findIssue(issue.id);
- targetIssue.removeAssignee(listFrom.assignee);
- } else if (listTo.type === 'milestone') {
- const currentMilestone = issue.milestone;
- const currentLists = this.state.lists
- .filter((list) => list.type === 'milestone' && list.id !== listTo.id)
- .filter((list) => list.issues.some((listIssue) => issue.id === listIssue.id));
-
- issue.removeMilestone(currentMilestone);
- issue.addMilestone(listTo.milestone);
- currentLists.forEach((currentList) => currentList.removeIssue(issue));
- listTo.addIssue(issue, listFrom, newIndex);
- } else {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
- }
- } else {
- listTo.updateIssueLabel(issue, listFrom);
- issueTo.removeLabel(listFrom.label);
- }
-
- if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
- issueLists.forEach((list) => {
- list.removeIssue(issue);
- });
- issue.removeLabels(listLabels);
- } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
- issue.removeAssignee(listFrom.assignee);
- listFrom.removeIssue(issue);
- } else if (listTo.type === 'backlog' && listFrom.type === 'milestone') {
- issue.removeMilestone(listFrom.milestone);
- listFrom.removeIssue(issue);
- } else if (this.shouldRemoveIssue(listFrom, listTo)) {
- listFrom.removeIssue(issue);
- }
- },
- shouldRemoveIssue(listFrom, listTo) {
- return (
- (listTo.type !== 'label' && listFrom.type === 'assignee') ||
- (listTo.type !== 'assignee' && listFrom.type === 'label') ||
- listFrom.type === 'backlog' ||
- listFrom.type === 'closed'
- );
- },
- moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
- const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
- const afterId = parseInt(idArray[newIndex + 1], 10) || null;
-
- list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
- },
- moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) {
- const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
- const afterId = parseInt(idArray[newIndex + issues.length], 10) || null;
- list.moveMultipleIssues({
- issues,
- oldIndicies,
- newIndex,
- moveBeforeId: beforeId,
- moveAfterId: afterId,
- });
- },
- findList(key, val) {
- return this.state.lists.find((list) => list[key] === val);
- },
- findListByLabelId(id) {
- return this.state.lists.find((list) => list.type === 'label' && list.label.id === id);
- },
-
- toggleFilter(filter) {
- const filterPath = this.filter.path.split('&');
- const filterIndex = filterPath.indexOf(filter);
-
- if (filterIndex === -1) {
- filterPath.push(filter);
- } else {
- filterPath.splice(filterIndex, 1);
- }
-
- this.filter.path = filterPath.join('&');
-
- this.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
- },
-
- setListDetail(newList) {
- this.detail.list = newList;
- },
-
- updateFiltersUrl() {
- window.history.pushState(null, null, `?${this.filter.path}`);
- },
-
- clearDetailIssue() {
- this.setIssueDetail({});
- },
-
- setIssueDetail(issueDetail) {
- this.detail.issue = issueDetail;
- },
-
- setTimeTrackingLimitToHours(limitToHours) {
- this.timeTracking.limitToHours = parseBoolean(limitToHours);
- },
-
- generateBoardGid(boardId) {
- return `gid://gitlab/Board/${boardId}`;
- },
-
- generateBoardsPath(id) {
- return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
- },
-
- generateIssuesPath(id) {
- return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`;
- },
-
- generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
- id ? `/${id}` : ''
- }`;
- },
-
- generateMultiDragPath(boardId) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`;
- },
-
- all() {
- return axios.get(this.state.endpoints.listsEndpoint);
- },
-
- createList(entityId, entityType) {
- const list = {
- [entityType]: entityId,
- };
-
- return axios.post(this.state.endpoints.listsEndpoint, {
- list,
- });
- },
-
- updateList(id, position, collapsed) {
- return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
- list: {
- position,
- collapsed,
- },
- });
- },
-
- updateListFunc(list) {
- const collapsed = !list.isExpanded;
- return this.updateList(list.id, list.position, collapsed).catch(() => {
- // TODO: handle request error
- });
- },
-
- destroyList(id) {
- return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
- },
- destroy(list) {
- const index = this.state.lists.indexOf(list);
- this.state.lists.splice(index, 1);
- this.updateNewListDropdown(list.id);
-
- this.destroyList(list.id).catch(() => {
- // TODO: handle request error
- });
- },
-
- saveList(list) {
- const entity = list.label || list.assignee || list.milestone || list.iteration;
- let entityType = '';
- if (list.label) {
- entityType = 'label_id';
- } else if (list.assignee) {
- entityType = 'assignee_id';
- } else if (IS_EE && list.milestone) {
- entityType = 'milestone_id';
- } else if (IS_EE && list.iteration) {
- entityType = 'iteration_id';
- }
-
- return this.createList(entity.id, entityType)
- .then((res) => res.data)
- .then((data) => {
- list.id = data.id;
- list.type = data.list_type;
- list.position = data.position;
- list.label = data.label;
-
- return list.getIssues();
- });
- },
-
- getListIssues(list, emptyIssues = true) {
- const data = {
- ...queryToObject(this.filter.path, { gatherArrays: true }),
- page: list.page,
- };
-
- if (list.label && data.label_name) {
- data.label_name = data.label_name.filter((label) => label !== list.label.title);
- }
-
- if (emptyIssues) {
- list.loading = true;
- }
-
- return this.getIssuesForList(list.id, data)
- .then((res) => res.data)
- .then((data) => {
- list.loading = false;
- list.issuesSize = data.size;
-
- if (emptyIssues) {
- list.issues = [];
- }
-
- data.issues.forEach((issueObj) => {
- list.addIssue(new ListIssue(issueObj));
- });
-
- return data;
- });
- },
-
- getIssuesForList(id, filter = {}) {
- const data = { id };
- Object.keys(filter).forEach((key) => {
- data[key] = filter[key];
- });
-
- return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
- },
-
- moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
- return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), {
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- });
- },
-
- moveListIssues(list, issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
- list.issues.splice(oldIndex, 1);
- list.issues.splice(newIndex, 0, issue);
-
- this.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
- // TODO: handle request error
- });
- },
-
- moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) {
- return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), {
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- ids,
- });
- },
-
- moveListMultipleIssues({ list, issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
- oldIndicies.reverse().forEach((index) => {
- list.issues.splice(index, 1);
- });
- list.issues.splice(newIndex, 0, ...issues);
-
- return this.moveMultipleIssues({
- ids: issues.map((issue) => issue.id),
- fromListId: null,
- toListId: null,
- moveBeforeId,
- moveAfterId,
- });
- },
-
- newIssue(id, issue) {
- if (typeof id === 'string') {
- id = getIdFromGraphQLId(id);
- }
-
- return axios.post(this.generateIssuesPath(id), {
- issue,
- });
- },
-
- newListIssue(list, issue) {
- list.addIssue(issue, null, 0);
- list.issuesSize += 1;
- let listId = list.id;
- if (typeof listId === 'string') {
- listId = getIdFromGraphQLId(listId);
- }
-
- return this.newIssue(list.id, issue)
- .then((res) => res.data)
- .then((data) => list.onNewIssueResponse(issue, data));
- },
-
- getBacklog(data) {
- return axios.get(
- mergeUrlParams(
- data,
- `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`,
- ),
- );
- },
- removeIssueLabel(issue, removeLabel) {
- if (removeLabel) {
- issue.labels = issue.labels.filter((label) => removeLabel.id !== label.id);
- }
- },
-
- addIssueAssignee(issue, assignee) {
- if (!issue.findAssignee(assignee)) {
- issue.assignees.push(new ListAssignee(assignee));
- }
- },
-
- setIssueAssignees(issue, assignees) {
- issue.assignees = [...assignees];
- },
-
- removeIssueLabels(issue, labels) {
- labels.forEach(issue.removeLabel.bind(issue));
- },
-
- bulkUpdate(issueIds, extraData = {}) {
- const data = {
- update: Object.assign(extraData, {
- issuable_ids: issueIds.join(','),
- }),
- };
-
- return axios.post(this.state.endpoints.bulkUpdatePath, data);
- },
-
- getIssueInfo(endpoint) {
- return axios.get(endpoint);
- },
-
- toggleIssueSubscription(endpoint) {
- return axios.post(endpoint);
- },
-
- recentBoards() {
- return axios.get(this.state.endpoints.recentBoardsEndpoint);
- },
-
- setCurrentBoard(board) {
- this.state.currentBoard = board;
- },
-
- toggleMultiSelect(issue) {
- const selectedIssueIds = this.multiSelect.list.map((issue) => issue.id);
- const index = selectedIssueIds.indexOf(issue.id);
-
- if (index === -1) {
- this.multiSelect.list.push(issue);
- return;
- }
-
- this.multiSelect.list = [
- ...this.multiSelect.list.slice(0, index),
- ...this.multiSelect.list.slice(index + 1),
- ];
- },
- removeIssueAssignee(issue, removeAssignee) {
- if (removeAssignee) {
- issue.assignees = issue.assignees.filter((assignee) => assignee.id !== removeAssignee.id);
- }
- },
-
- findIssueAssignee(issue, findAssignee) {
- return issue.assignees.find((assignee) => assignee.id === findAssignee.id);
- },
-
- clearMultiSelect() {
- this.multiSelect.list = [];
- },
-
- removeAllIssueAssignees(issue) {
- issue.assignees = [];
- },
-
- addIssueMilestone(issue, milestone) {
- const miletoneId = issue.milestone ? issue.milestone.id : null;
- if (IS_EE && milestone.id !== miletoneId) {
- issue.milestone = new ListMilestone(milestone);
- }
- },
-
- setIssueLoadingState(issue, key, value) {
- issue.isLoading[key] = value;
- },
-
- updateIssueData(issue, newData) {
- Object.assign(issue, newData);
- },
-
- setIssueFetchingState(issue, key, value) {
- issue.isFetching[key] = value;
- },
-
- removeIssueMilestone(issue, removeMilestone) {
- if (IS_EE && removeMilestone && removeMilestone.id === issue.milestone.id) {
- issue.milestone = {};
- }
- },
-
- refreshIssueData(issue, obj) {
- const convertedObj = convertObjectPropsToCamelCase(obj, {
- dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
- });
- convertedObj.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
- issue.path = obj.real_path || obj.webUrl;
- issue.project_id = obj.project_id;
- Object.assign(issue, convertedObj);
-
- if (obj.project) {
- issue.project = new IssueProject(obj.project);
- }
-
- if (obj.milestone) {
- issue.milestone = new ListMilestone(obj.milestone);
- issue.milestone_id = obj.milestone.id;
- }
-
- if (obj.labels) {
- issue.labels = obj.labels.map((label) => new ListLabel(label));
- }
-
- if (obj.assignees) {
- issue.assignees = obj.assignees.map((a) => new ListAssignee(a));
- }
- },
- addIssueLabel(issue, label) {
- if (!issue.findLabel(label)) {
- issue.labels.push(new ListLabel(label));
- }
- },
- updateIssue(issue) {
- const data = {
- issue: {
- milestone_id: issue.milestone ? issue.milestone.id : null,
- due_date: issue.dueDate,
- assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0],
- label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''],
- },
- };
-
- return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => {
- /**
- * Since post implementation of Scoped labels, server can reject
- * same key-ed labels. To keep the UI and server Model consistent,
- * we're just assigning labels that server echo's back to us when we
- * PATCH the said object.
- */
- if (body) {
- issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
- }
- });
- },
-};
-
-BoardsStoreEE.initEESpecific(boardsStore);
-
-// hacks added in order to allow milestone_select to function properly
-// TODO: remove these
-
-export function boardStoreIssueSet(...args) {
- Vue.set(boardsStore.detail.issue, ...args);
-}
-
-export function boardStoreIssueDelete(...args) {
- Vue.delete(boardsStore.detail.issue, ...args);
-}
-
-export default boardsStore;
diff --git a/app/assets/javascripts/boards/stores/boards_store_ee.js b/app/assets/javascripts/boards/stores/boards_store_ee.js
deleted file mode 100644
index 2a289ce5d0a..00000000000
--- a/app/assets/javascripts/boards/stores/boards_store_ee.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// this is just to make ee_else_ce happy and will be cleaned up in https://gitlab.com/gitlab-org/gitlab-foss/issues/59807
-
-export default {
- initEESpecific() {},
-};
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index 140c9ef7ac4..cb31eb4b008 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -16,7 +16,7 @@ export default {
},
activeBoardItem: (state) => {
- return state.boardItems[state.activeId] || { iid: '', id: '', fullId: '' };
+ return state.boardItems[state.activeId] || { iid: '', id: '' };
},
groupPathForActiveIssue: (_, getters) => {
@@ -51,8 +51,4 @@ export default {
isEpicBoard: () => {
return false;
},
-
- shouldUseGraphQL: () => {
- return gon?.features?.graphqlBoardLists;
- },
};
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 31b78014525..928cece19f7 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -41,3 +41,7 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
export const SET_ERROR = 'SET_ERROR';
+
+export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
+export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
+export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 668a3dbaa7e..ef5b84b4575 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -1,6 +1,5 @@
import { cloneDeep, pull, union } from 'lodash';
import Vue from 'vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, __ } from '~/locale';
import { formatIssue } from '../boards_util';
import { issuableTypes } from '../constants';
@@ -65,6 +64,20 @@ export default {
);
},
+ [mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
+ state.iterationsLoading = true;
+ },
+
+ [mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
+ state.iterations = iterations;
+ state.iterationsLoading = false;
+ },
+
+ [mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
+ state.iterationsLoading = false;
+ state.error = __('Failed to load iterations.');
+ },
+
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id;
state.sidebarType = sidebarType;
@@ -187,8 +200,7 @@ export default {
},
[mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => {
- const issueId = getIdFromGraphQLId(issue.id);
- Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId }));
+ Vue.set(state.boardItems, issue.id, formatIssue(issue));
},
[mutationTypes.ADD_BOARD_ITEM_TO_LIST]: (
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
index 264a03ff39d..80c51c966d2 100644
--- a/app/assets/javascripts/boards/stores/state.js
+++ b/app/assets/javascripts/boards/stores/state.js
@@ -31,6 +31,8 @@ export default () => ({
},
selectedProject: {},
error: undefined,
+ iterations: [],
+ iterationsLoading: false,
addColumnForm: {
visible: false,
columnType: ListType.label,
diff --git a/app/assets/javascripts/captcha/init_recaptcha_script.js b/app/assets/javascripts/captcha/init_recaptcha_script.js
index f546eef7d84..28aef22873d 100644
--- a/app/assets/javascripts/captcha/init_recaptcha_script.js
+++ b/app/assets/javascripts/captcha/init_recaptcha_script.js
@@ -1,7 +1,7 @@
// NOTE: This module will be used in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52044
import { memoize } from 'lodash';
-export const RECAPTCHA_API_URL_PREFIX = 'https://www.google.com/recaptcha/api.js';
+export const RECAPTCHA_API_URL_PREFIX = window.gon.recaptcha_api_server_url;
export const RECAPTCHA_ONLOAD_CALLBACK_NAME = 'recaptchaOnloadCallback';
/**
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 03fd600e493..8e527e2bff6 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -337,7 +337,7 @@ export default {
</gl-collapse>
<gl-alert
v-if="containsVariableReference"
- :title="__('Value may contain a variable reference')"
+ :title="__('Value might contain a variable reference')"
:dismissible="false"
variant="warning"
data-testid="contains-variable-reference"
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index f4002537f79..4ebbf05814b 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -26,5 +26,5 @@ export const AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';
export const AWS_TOKEN_CONSTANTS = [AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY];
export const CONTAINS_VARIABLE_REFERENCE_MESSAGE = __(
- 'Variable references indicated by %{codeStart}$%{codeEnd} may be expanded. If this is not what you want, consider %{docsLinkStart}using a workaround to prevent expansion%{docsLinkEnd}.',
+ 'Values that contain the %{codeStart}$%{codeEnd} character can be considered a variable reference and expanded. %{docsLinkStart}Learn more.%{docsLinkEnd}',
);
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index c2c035963f4..8dcab55ac61 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -218,14 +218,14 @@ export default class Clusters {
}
setBannerDismissedState(status, isDismissed) {
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
}
}
isBannerDismissed(status) {
let bannerState;
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
bannerState = window.localStorage.getItem(this.clusterBannerDismissedKey);
}
@@ -233,7 +233,7 @@ export default class Clusters {
}
setClusterNewlyCreated(state) {
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
window.localStorage.setItem(this.clusterNewlyCreatedKey, Boolean(state));
}
}
@@ -242,7 +242,7 @@ export default class Clusters {
// once this is true, it will always be true for a given page load
if (!this.isNewlyCreated) {
let newlyCreated;
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
newlyCreated = window.localStorage.getItem(this.clusterNewlyCreatedKey);
}
diff --git a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
index b9c55409330..0da7be4040f 100644
--- a/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
+++ b/app/assets/javascripts/clusters/components/remove_cluster_confirmation.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
import { escape } from 'lodash';
import csrf from '~/lib/utils/csrf';
@@ -141,7 +140,7 @@ export default {
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</ul>
</div>
- <strong v-html="confirmationTextLabel"></strong>
+ <strong v-html="confirmationTextLabel /* eslint-disable-line vue/no-v-html */"></strong>
<form ref="form" :action="clusterPath" method="post" class="gl-mb-5">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 8f81d967126..0d1534d20e0 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -205,6 +205,8 @@ export default {
:items="clusters"
:fields="fields"
stacked="md"
+ head-variant="white"
+ thead-class="gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
class="qa-clusters-table"
data-testid="cluster_list_table"
>
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 5f24a3c370a..580db871f5f 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,7 +1,6 @@
/* eslint-disable func-names, consistent-return, one-var, no-return-assign */
import $ from 'jquery';
-import 'jquery.waitforimages';
// Width where images must fits in, for 2-up this gets divided by 2
const availWidth = 900;
@@ -16,11 +15,7 @@ export default class ImageFile {
// Load two-up view after images are loaded
// so that we can display the correct width and height information
- const $images = $('.two-up.view img', this.file);
-
- $images.waitForImages(() => {
- this.initView('two-up');
- });
+ this.initView('two-up');
}),
);
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 8d88b682df2..2109aecdf03 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { initPipelineCountListener } from './utils';
/**
* Used in:
@@ -12,13 +13,7 @@ export default () => {
if (pipelineTableViewEl) {
// Update MR and Commits tabs
- pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => {
- if (event.detail.pipelineCount) {
- const badge = document.querySelector('.js-pipelines-mr-count');
-
- badge.textContent = event.detail.pipelineCount;
- }
- });
+ initPipelineCountListener(pipelineTableViewEl);
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
const table = new Vue({
diff --git a/app/assets/javascripts/commit/pipelines/utils.js b/app/assets/javascripts/commit/pipelines/utils.js
new file mode 100644
index 00000000000..52cbe52fa9b
--- /dev/null
+++ b/app/assets/javascripts/commit/pipelines/utils.js
@@ -0,0 +1,11 @@
+export function initPipelineCountListener(el) {
+ if (!el) return;
+
+ el.addEventListener('update-pipelines-count', (event) => {
+ if (event.detail.pipelineCount) {
+ const badge = document.querySelector('.js-pipelines-mr-count');
+
+ badge.textContent = event.detail.pipelineCount;
+ }
+ });
+}
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 5f778af1dbb..59066162960 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
@@ -96,12 +96,23 @@ export default {
}
},
},
+ i18n: {
+ project: __('Project'),
+ privateForkSelected: __(
+ "To protect this issue's confidentiality, a private fork of this project was selected.",
+ ),
+ noForks: __('No forks are available to you.'),
+ forkTheProject: __(
+ `To protect this issue's confidentiality, %{linkStart}fork this project%{linkEnd} and set the fork's visibility to private.`,
+ ),
+ readMore: __('Read more'),
+ },
};
</script>
<template>
<div class="confidential-merge-request-fork-group form-group">
- <label>{{ __('Project') }}</label>
+ <label>{{ $options.i18n.project }}</label>
<div>
<dropdown
v-if="projects.length"
@@ -111,25 +122,13 @@ export default {
/>
<p class="text-muted mt-1 mb-0">
<template v-if="projects.length">
- {{
- __(
- "To protect this issue's confidentiality, a private fork of this project was selected.",
- )
- }}
+ {{ $options.i18n.privateForkSelected }}
</template>
<template v-else>
- {{ __('No forks are available to you.') }}<br />
- <gl-sprintf
- :message="
- __(
- `To protect this issue's confidentiality, %{forkLink} and set the fork's visibility to private.`,
- )
- "
- >
- <template #forkLink>
- <a :href="newForkPath" target="_blank" class="help-link">{{
- __('fork this project')
- }}</a>
+ {{ $options.i18n.noForks }}<br />
+ <gl-sprintf :message="$options.i18n.forkTheProject">
+ <template #link="{ content }">
+ <a :href="newForkPath" target="_blank" class="help-link">{{ content }}</a>
</template>
</gl-sprintf>
</template>
@@ -138,7 +137,7 @@ export default {
class="w-auto p-0 d-inline-block text-primary bg-transparent"
target="_blank"
>
- <span class="sr-only">{{ __('Read more') }}</span>
+ <span class="sr-only">{{ $options.i18n.readMore }}</span>
<gl-icon name="question-o" />
</gl-link>
</p>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index a372233e543..02ab34447ca 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -100,11 +100,13 @@ export default {
:class="{ 'is-focused': focused }"
>
<top-toolbar ref="toolbar" class="gl-mb-4" />
- <formatting-bubble-menu />
<div v-if="isLoadingContent" class="gl-w-full gl-display-flex gl-justify-content-center">
<gl-loading-icon size="sm" />
</div>
- <tiptap-editor-content v-else class="md" :editor="contentEditor.tiptapEditor" />
+ <template v-else>
+ <formatting-bubble-menu />
+ <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
+ </template>
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
index 6c00480b87e..14a553ff30b 100644
--- a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
@@ -20,7 +20,11 @@ export default {
};
</script>
<template>
- <bubble-menu class="gl-shadow gl-rounded-base" :editor="tiptapEditor">
+ <bubble-menu
+ data-testid="formatting-bubble-menu"
+ class="gl-shadow gl-rounded-base"
+ :editor="tiptapEditor"
+ >
<gl-button-group>
<toolbar-button
data-testid="bold"
diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue
index 3762324a431..5b81e5fddcc 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/image.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/image.vue
@@ -22,6 +22,7 @@ export default {
<img
data-testid="image"
class="gl-max-w-full gl-h-auto"
+ :title="node.attrs.title"
:class="{ 'gl-opacity-5': node.attrs.uploading }"
:src="node.attrs.src"
/>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
new file mode 100644
index 00000000000..c44e8145982
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -0,0 +1,142 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+import { selectedRect as getSelectedRect } from 'prosemirror-tables';
+import { __ } from '~/locale';
+
+const TABLE_CELL_HEADER = 'th';
+const TABLE_CELL_BODY = 'td';
+
+export default {
+ name: 'TableCellBaseWrapper',
+ components: {
+ NodeViewWrapper,
+ NodeViewContent,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ },
+ props: {
+ cellType: {
+ type: String,
+ validator: (type) => [TABLE_CELL_HEADER, TABLE_CELL_BODY].includes(type),
+ required: true,
+ },
+ editor: {
+ type: Object,
+ required: true,
+ },
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ displayActionsDropdown: false,
+ preventHide: true,
+ selectedRect: null,
+ };
+ },
+ computed: {
+ totalRows() {
+ return this.selectedRect?.map.height;
+ },
+ totalCols() {
+ return this.selectedRect?.map.width;
+ },
+ isTableBodyCell() {
+ return this.cellType === TABLE_CELL_BODY;
+ },
+ },
+ mounted() {
+ this.editor.on('selectionUpdate', this.handleSelectionUpdate);
+ this.handleSelectionUpdate();
+ },
+ beforeDestroy() {
+ this.editor.off('selectionUpdate', this.handleSelectionUpdate);
+ },
+ methods: {
+ handleSelectionUpdate() {
+ const { state } = this.editor;
+ const { $cursor } = state.selection;
+
+ this.displayActionsDropdown = $cursor?.pos - $cursor?.parentOffset - 1 === this.getPos();
+ if (this.displayActionsDropdown) {
+ this.selectedRect = getSelectedRect(state);
+ }
+ },
+ runCommand(command) {
+ this.editor.chain()[command]().run();
+ this.hideDropdown();
+ },
+ handleHide($event) {
+ if (this.preventHide) {
+ $event.preventDefault();
+ }
+ this.preventHide = true;
+ },
+ hideDropdown() {
+ this.preventHide = false;
+ this.$refs.dropdown?.hide();
+ },
+ },
+ i18n: {
+ insertColumnBefore: __('Insert column before'),
+ insertColumnAfter: __('Insert column after'),
+ insertRowBefore: __('Insert row before'),
+ insertRowAfter: __('Insert row after'),
+ deleteRow: __('Delete row'),
+ deleteColumn: __('Delete column'),
+ deleteTable: __('Delete table'),
+ editTableActions: __('Edit table'),
+ },
+};
+</script>
+<template>
+ <node-view-wrapper
+ class="gl-relative gl-padding-5 gl-min-w-10"
+ :as="cellType"
+ @click="hideDropdown"
+ >
+ <span v-if="displayActionsDropdown" class="gl-absolute gl-right-0 gl-top-0">
+ <gl-dropdown
+ ref="dropdown"
+ dropup
+ icon="chevron-down"
+ size="small"
+ category="tertiary"
+ boundary="viewport"
+ no-caret
+ text-sr-only
+ :text="$options.i18n.editTableActions"
+ :popper-opts="{ positionFixed: true }"
+ @hide="handleHide($event)"
+ >
+ <gl-dropdown-item @click="runCommand('addColumnBefore')">
+ {{ $options.i18n.insertColumnBefore }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="runCommand('addColumnAfter')">
+ {{ $options.i18n.insertColumnAfter }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="isTableBodyCell" @click="runCommand('addRowBefore')">
+ {{ $options.i18n.insertRowBefore }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="runCommand('addRowAfter')">
+ {{ $options.i18n.insertRowAfter }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-item v-if="totalRows > 2 && isTableBodyCell" @click="runCommand('deleteRow')">
+ {{ $options.i18n.deleteRow }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-if="totalCols > 1" @click="runCommand('deleteColumn')">
+ {{ $options.i18n.deleteColumn }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="runCommand('deleteTable')">
+ {{ $options.i18n.deleteTable }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </span>
+ <node-view-content />
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
new file mode 100644
index 00000000000..6b4343dd5b8
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_body.vue
@@ -0,0 +1,23 @@
+<script>
+import TableCellBase from './table_cell_base.vue';
+
+export default {
+ name: 'TableCellBody',
+ components: {
+ TableCellBase,
+ },
+ props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <table-cell-base cell-type="td" v-bind="$props" />
+</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
new file mode 100644
index 00000000000..5f9889374f6
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_header.vue
@@ -0,0 +1,23 @@
+<script>
+import TableCellBase from './table_cell_base.vue';
+
+export default {
+ name: 'TableCellHeader',
+ components: {
+ TableCellBase,
+ },
+ props: {
+ editor: {
+ type: Object,
+ required: true,
+ },
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <table-cell-base cell-type="th" v-bind="$props" />
+</template>
diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js
index f277508f628..4af9dc8e405 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants.js
@@ -45,3 +45,7 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
export const LOADING_CONTENT_EVENT = 'loadingContent';
export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
export const LOADING_ERROR_EVENT = 'loadingError';
+
+export const PARSE_HTML_PRIORITY_LOWEST = 1;
+export const PARSE_HTML_PRIORITY_DEFAULT = 50;
+export const PARSE_HTML_PRIORITY_HIGHEST = 100;
diff --git a/app/assets/javascripts/content_editor/content_editor.stories.js b/app/assets/javascripts/content_editor/content_editor.stories.js
new file mode 100644
index 00000000000..8f2ce8feb5d
--- /dev/null
+++ b/app/assets/javascripts/content_editor/content_editor.stories.js
@@ -0,0 +1,27 @@
+import { ContentEditor } from './index';
+
+export default {
+ component: ContentEditor,
+ title: 'Components/Content Editor',
+};
+
+const Template = (_, { argTypes }) => ({
+ components: { ContentEditor },
+ props: Object.keys(argTypes),
+ template: '<content-editor v-bind="$props" @initialized="loadContent" />',
+ methods: {
+ loadContent(contentEditor) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ contentEditor.setSerializedContent('Hello content editor');
+ },
+ },
+});
+
+export const Default = Template.bind({});
+
+Default.args = {
+ renderMarkdown: () => '<p>Hello content editor</p>',
+ uploadsPath: '/uploads/',
+ serializerConfig: {},
+ extensions: [],
+};
diff --git a/app/assets/javascripts/content_editor/extensions/audio.js b/app/assets/javascripts/content_editor/extensions/audio.js
new file mode 100644
index 00000000000..25d4068c93f
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/audio.js
@@ -0,0 +1,9 @@
+import Playable from './playable';
+
+export default Playable.extend({
+ name: 'audio',
+ defaultOptions: {
+ ...Playable.options,
+ mediaType: 'audio',
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js
index 45f53fe230b..4512ead44bc 100644
--- a/app/assets/javascripts/content_editor/extensions/blockquote.js
+++ b/app/assets/javascripts/content_editor/extensions/blockquote.js
@@ -1 +1,33 @@
-export { Blockquote as default } from '@tiptap/extension-blockquote';
+import { Blockquote } from '@tiptap/extension-blockquote';
+import { wrappingInputRule } from 'prosemirror-inputrules';
+import { getParents } from '~/lib/utils/dom_utils';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
+
+export const multilineInputRegex = /^\s*>>>\s$/gm;
+
+export default Blockquote.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+
+ multiline: {
+ default: false,
+ parseHTML: (element) => {
+ const source = getMarkdownSource(element);
+ const parentsIncludeBlockquote = getParents(element).some(
+ (p) => p.nodeName.toLowerCase() === 'blockquote',
+ );
+
+ return source && !source.startsWith('>') && !parentsIncludeBlockquote;
+ },
+ },
+ };
+ },
+
+ addInputRules() {
+ return [
+ ...this.parent?.(),
+ wrappingInputRule(multilineInputRegex, this.type, () => ({ multiline: true })),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/bullet_list.js b/app/assets/javascripts/content_editor/extensions/bullet_list.js
index 01ead571fe1..8d0faf7a9fe 100644
--- a/app/assets/javascripts/content_editor/extensions/bullet_list.js
+++ b/app/assets/javascripts/content_editor/extensions/bullet_list.js
@@ -1 +1,19 @@
-export { BulletList as default } from '@tiptap/extension-bullet-list';
+import { BulletList } from '@tiptap/extension-bullet-list';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
+
+export default BulletList.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+
+ bullet: {
+ default: '*',
+ parseHTML(element) {
+ const bullet = getMarkdownSource(element)?.charAt(0);
+
+ return '*+-'.includes(bullet) ? bullet : '*';
+ },
+ },
+ };
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index c6d32fb8547..25f5837d2a6 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -8,11 +8,7 @@ export default CodeBlockLowlight.extend({
return {
language: {
default: null,
- parseHTML: (element) => {
- return {
- language: extractLanguage(element),
- };
- },
+ parseHTML: (element) => extractLanguage(element),
},
class: {
default: 'code highlight js-syntax-highlight',
diff --git a/app/assets/javascripts/content_editor/extensions/description_item.js b/app/assets/javascripts/content_editor/extensions/description_item.js
new file mode 100644
index 00000000000..957fdede27b
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/description_item.js
@@ -0,0 +1,49 @@
+import { Node, mergeAttributes } from '@tiptap/core';
+
+export default Node.create({
+ name: 'descriptionItem',
+ content: 'block+',
+ defining: true,
+
+ addAttributes() {
+ return {
+ isTerm: {
+ default: true,
+ parseHTML: (element) => element.tagName.toLowerCase() === 'dt',
+ },
+ };
+ },
+
+ parseHTML() {
+ return [{ tag: 'dt' }, { tag: 'dd' }];
+ },
+
+ renderHTML({ HTMLAttributes: { isTerm, ...HTMLAttributes } }) {
+ return [
+ 'li',
+ mergeAttributes(HTMLAttributes, { class: isTerm ? 'dl-term' : 'dl-description' }),
+ 0,
+ ];
+ },
+
+ addKeyboardShortcuts() {
+ return {
+ Enter: () => {
+ return this.editor.commands.splitListItem('descriptionItem');
+ },
+ Tab: () => {
+ const { isTerm } = this.editor.getAttributes('descriptionItem');
+ if (isTerm)
+ return this.editor.commands.updateAttributes('descriptionItem', { isTerm: !isTerm });
+
+ return false;
+ },
+ 'Shift-Tab': () => {
+ const { isTerm } = this.editor.getAttributes('descriptionItem');
+ if (isTerm) return this.editor.commands.liftListItem('descriptionItem');
+
+ return this.editor.commands.updateAttributes('descriptionItem', { isTerm: true });
+ },
+ };
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/description_list.js b/app/assets/javascripts/content_editor/extensions/description_list.js
new file mode 100644
index 00000000000..a516dfad2b8
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/description_list.js
@@ -0,0 +1,23 @@
+import { Node, mergeAttributes } from '@tiptap/core';
+import { wrappingInputRule } from 'prosemirror-inputrules';
+
+export const inputRegex = /^\s*(<dl>)$/;
+
+export default Node.create({
+ name: 'descriptionList',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ group: 'block list',
+ content: 'descriptionItem+',
+
+ parseHTML() {
+ return [{ tag: 'dl' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['ul', mergeAttributes(HTMLAttributes, { class: 'dl-content' }), 0];
+ },
+
+ addInputRules() {
+ return [wrappingInputRule(inputRegex, this.type)];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/division.js b/app/assets/javascripts/content_editor/extensions/division.js
new file mode 100644
index 00000000000..c70d1700941
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/division.js
@@ -0,0 +1,17 @@
+import { Node } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
+
+export default Node.create({
+ name: 'division',
+ content: 'block*',
+ group: 'block',
+ defining: true,
+
+ parseHTML() {
+ return [{ tag: 'div', priority: PARSE_HTML_PRIORITY_LOWEST }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['div', HTMLAttributes, 0];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index d88b9f92215..de608c3aaa2 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -17,30 +17,18 @@ export default Node.create({
return {
moji: {
default: null,
- parseHTML: (element) => {
- return {
- moji: element.textContent,
- };
- },
+ parseHTML: (element) => element.textContent,
},
name: {
default: null,
- parseHTML: (element) => {
- return {
- name: element.dataset.name,
- };
- },
+ parseHTML: (element) => element.dataset.name,
},
title: {
default: null,
},
unicodeVersion: {
default: '6.0',
- parseHTML: (element) => {
- return {
- unicodeVersion: element.dataset.unicodeVersion,
- };
- },
+ parseHTML: (element) => element.dataset.unicodeVersion,
},
};
},
diff --git a/app/assets/javascripts/content_editor/extensions/figure.js b/app/assets/javascripts/content_editor/extensions/figure.js
new file mode 100644
index 00000000000..b2076894412
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/figure.js
@@ -0,0 +1,16 @@
+import { Node } from '@tiptap/core';
+
+export default Node.create({
+ name: 'figure',
+ content: 'block+',
+ group: 'block',
+ defining: true,
+
+ parseHTML() {
+ return [{ tag: 'figure' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['figure', HTMLAttributes, 0];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/figure_caption.js b/app/assets/javascripts/content_editor/extensions/figure_caption.js
new file mode 100644
index 00000000000..ffd1b474f03
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/figure_caption.js
@@ -0,0 +1,16 @@
+import { Node } from '@tiptap/core';
+
+export default Node.create({
+ name: 'figureCaption',
+ content: 'inline*',
+ group: 'block',
+ defining: true,
+
+ parseHTML() {
+ return [{ tag: 'figcaption' }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return ['figcaption', HTMLAttributes, 0];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/html_marks.js b/app/assets/javascripts/content_editor/extensions/html_marks.js
new file mode 100644
index 00000000000..54adb9efa0c
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/html_marks.js
@@ -0,0 +1,66 @@
+import { Mark, mergeAttributes, markInputRule } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_LOWEST } from '../constants';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+const marks = [
+ 'ins',
+ 'abbr',
+ 'bdo',
+ 'cite',
+ 'dfn',
+ 'mark',
+ 'small',
+ 'span',
+ 'time',
+ 'kbd',
+ 'q',
+ 'samp',
+ 'var',
+ 'ruby',
+ 'rp',
+ 'rt',
+];
+
+const attrs = {
+ time: ['datetime'],
+ abbr: ['title'],
+ span: ['dir'],
+ bdo: ['dir'],
+};
+
+export default marks.map((name) =>
+ Mark.create({
+ name,
+
+ inclusive: false,
+
+ defaultOptions: {
+ HTMLAttributes: {},
+ },
+
+ addAttributes() {
+ return (attrs[name] || []).reduce(
+ (acc, attr) => ({
+ ...acc,
+ [attr]: {
+ default: null,
+ parseHTML: (element) => element.getAttribute(attr),
+ },
+ }),
+ {},
+ );
+ },
+
+ parseHTML() {
+ return [{ tag: name, priority: PARSE_HTML_PRIORITY_LOWEST }];
+ },
+
+ renderHTML({ HTMLAttributes }) {
+ return [name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
+ },
+
+ addInputRules() {
+ return [markInputRule(markInputRegex(name), this.type, extractMarkAttributesFromMatch)];
+ },
+ }),
+);
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index c9e8dfa4ad9..837fab0585f 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,6 +1,7 @@
import { Image } from '@tiptap/extension-image';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import ImageWrapper from '../components/wrappers/image.vue';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
const resolveImageEl = (element) =>
element.nodeName === 'IMG' ? element : element.querySelector('img');
@@ -27,27 +28,27 @@ export default Image.extend({
parseHTML: (element) => {
const img = resolveImageEl(element);
- return {
- src: img.dataset.src || img.getAttribute('src'),
- };
+ return img.dataset.src || img.getAttribute('src');
},
},
canonicalSrc: {
default: null,
+ parseHTML: (element) => element.dataset.canonicalSrc,
+ },
+ alt: {
+ default: null,
parseHTML: (element) => {
- return {
- canonicalSrc: element.dataset.canonicalSrc,
- };
+ const img = resolveImageEl(element);
+
+ return img.getAttribute('alt');
},
},
- alt: {
+ title: {
default: null,
parseHTML: (element) => {
const img = resolveImageEl(element);
- return {
- alt: img.getAttribute('alt'),
- };
+ return img.getAttribute('title');
},
},
};
@@ -55,7 +56,7 @@ export default Image.extend({
parseHTML() {
return [
{
- priority: 100,
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
tag: 'a.no-attachment-icon',
},
{
diff --git a/app/assets/javascripts/content_editor/extensions/inline_diff.js b/app/assets/javascripts/content_editor/extensions/inline_diff.js
index 9471d324764..3bd328958df 100644
--- a/app/assets/javascripts/content_editor/extensions/inline_diff.js
+++ b/app/assets/javascripts/content_editor/extensions/inline_diff.js
@@ -14,11 +14,7 @@ export default Mark.create({
return {
type: {
default: 'addition',
- parseHTML: (element) => {
- return {
- type: element.classList.contains('deletion') ? 'deletion' : 'addition',
- };
- },
+ parseHTML: (element) => (element.classList.contains('deletion') ? 'deletion' : 'addition'),
},
};
},
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index 53104fe07a3..fc0f38e6935 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -36,19 +36,15 @@ export default Link.extend({
...this.parent?.(),
href: {
default: null,
- parseHTML: (element) => {
- return {
- href: element.getAttribute('href'),
- };
- },
+ parseHTML: (element) => element.getAttribute('href'),
+ },
+ title: {
+ title: null,
+ parseHTML: (element) => element.getAttribute('title'),
},
canonicalSrc: {
default: null,
- parseHTML: (element) => {
- return {
- canonicalSrc: element.dataset.canonicalSrc,
- };
- },
+ parseHTML: (element) => element.dataset.canonicalSrc,
},
};
},
diff --git a/app/assets/javascripts/content_editor/extensions/ordered_list.js b/app/assets/javascripts/content_editor/extensions/ordered_list.js
index 9a79187d9c1..57d5bd6ebf8 100644
--- a/app/assets/javascripts/content_editor/extensions/ordered_list.js
+++ b/app/assets/javascripts/content_editor/extensions/ordered_list.js
@@ -1 +1,15 @@
-export { OrderedList as default } from '@tiptap/extension-ordered-list';
+import { OrderedList } from '@tiptap/extension-ordered-list';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
+
+export default OrderedList.extend({
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+
+ parens: {
+ default: false,
+ parseHTML: (element) => /^[0-9]+\)/.test(getMarkdownSource(element)),
+ },
+ };
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
new file mode 100644
index 00000000000..0062bc563db
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -0,0 +1,66 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import { Node } from '@tiptap/core';
+
+const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
+
+export default Node.create({
+ group: 'inline',
+ inline: true,
+ draggable: true,
+
+ addAttributes() {
+ return {
+ src: {
+ default: null,
+ parseHTML: (element) => {
+ const playable = queryPlayableElement(element, this.options.mediaType);
+
+ return playable.src;
+ },
+ },
+ canonicalSrc: {
+ default: null,
+ parseHTML: (element) => {
+ const playable = queryPlayableElement(element, this.options.mediaType);
+
+ return playable.dataset.canonicalSrc;
+ },
+ },
+ alt: {
+ default: null,
+ parseHTML: (element) => {
+ const playable = queryPlayableElement(element, this.options.mediaType);
+
+ return playable.dataset.title;
+ },
+ },
+ };
+ },
+
+ parseHTML() {
+ return [
+ {
+ tag: `.${this.options.mediaType}-container`,
+ },
+ ];
+ },
+
+ renderHTML({ node }) {
+ return [
+ 'span',
+ { class: `media-container ${this.options.mediaType}-container` },
+ [
+ this.options.mediaType,
+ {
+ src: node.attrs.src,
+ controls: true,
+ 'data-setup': '{}',
+ 'data-title': node.attrs.alt,
+ ...this.extraElementAttrs,
+ },
+ ],
+ ['a', { href: node.attrs.src }, node.attrs.alt],
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index 5f4484af9c8..5e459e65de2 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -1,4 +1,10 @@
import { Node } from '@tiptap/core';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+
+const getAnchor = (element) => {
+ if (element.nodeName === 'A') return element;
+ return element.querySelector('a');
+};
export default Node.create({
name: 'reference',
@@ -13,43 +19,23 @@ export default Node.create({
return {
className: {
default: null,
- parseHTML: (element) => {
- return {
- className: element.className,
- };
- },
+ parseHTML: (element) => getAnchor(element).className,
},
referenceType: {
default: null,
- parseHTML: (element) => {
- return {
- referenceType: element.dataset.referenceType,
- };
- },
+ parseHTML: (element) => getAnchor(element).dataset.referenceType,
},
originalText: {
default: null,
- parseHTML: (element) => {
- return {
- originalText: element.dataset.original,
- };
- },
+ parseHTML: (element) => getAnchor(element).dataset.original,
},
href: {
default: null,
- parseHTML: (element) => {
- return {
- href: element.getAttribute('href'),
- };
- },
+ parseHTML: (element) => getAnchor(element).getAttribute('href'),
},
text: {
default: null,
- parseHTML: (element) => {
- return {
- text: element.textContent,
- };
- },
+ parseHTML: (element) => getAnchor(element).textContent,
},
};
},
@@ -58,7 +44,10 @@ export default Node.create({
return [
{
tag: 'a.gfm:not([data-link=true])',
- priority: 51,
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
+ },
+ {
+ tag: 'span.gl-label',
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/subscript.js b/app/assets/javascripts/content_editor/extensions/subscript.js
index 4bf89796efe..d0766f42308 100644
--- a/app/assets/javascripts/content_editor/extensions/subscript.js
+++ b/app/assets/javascripts/content_editor/extensions/subscript.js
@@ -1 +1,9 @@
-export { Subscript as default } from '@tiptap/extension-subscript';
+import { markInputRule } from '@tiptap/core';
+import { Subscript } from '@tiptap/extension-subscript';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+export default Subscript.extend({
+ addInputRules() {
+ return [markInputRule(markInputRegex('sub'), this.type, extractMarkAttributesFromMatch)];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/superscript.js b/app/assets/javascripts/content_editor/extensions/superscript.js
index 3eb7d86d90d..6cd814977ea 100644
--- a/app/assets/javascripts/content_editor/extensions/superscript.js
+++ b/app/assets/javascripts/content_editor/extensions/superscript.js
@@ -1 +1,9 @@
-export { Superscript as default } from '@tiptap/extension-superscript';
+import { markInputRule } from '@tiptap/core';
+import { Superscript } from '@tiptap/extension-superscript';
+import { markInputRegex, extractMarkAttributesFromMatch } from '../services/mark_utils';
+
+export default Superscript.extend({
+ addInputRules() {
+ return [markInputRule(markInputRegex('sup'), this.type, extractMarkAttributesFromMatch)];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index 5bdc39231a1..befc33e669f 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -1,5 +1,12 @@
import { TableCell } from '@tiptap/extension-table-cell';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import TableCellBodyWrapper from '../components/wrappers/table_cell_body.vue';
+import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableCell.extend({
- content: 'inline*',
+ content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+
+ addNodeView() {
+ return VueNodeViewRenderer(TableCellBodyWrapper);
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 23509706e4b..829b06fc14b 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,5 +1,11 @@
import { TableHeader } from '@tiptap/extension-table-header';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import TableCellHeaderWrapper from '../components/wrappers/table_cell_header.vue';
+import { isBlockTablesFeatureEnabled } from '../services/feature_flags';
export default TableHeader.extend({
- content: 'inline*',
+ content: isBlockTablesFeatureEnabled() ? 'block+' : 'inline*',
+ addNodeView() {
+ return VueNodeViewRenderer(TableCellHeaderWrapper);
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/task_item.js b/app/assets/javascripts/content_editor/extensions/task_item.js
index 6163c0e043b..9b050edcb28 100644
--- a/app/assets/javascripts/content_editor/extensions/task_item.js
+++ b/app/assets/javascripts/content_editor/extensions/task_item.js
@@ -1,4 +1,5 @@
import { TaskItem } from '@tiptap/extension-task-item';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
export default TaskItem.extend({
defaultOptions: {
@@ -12,7 +13,8 @@ export default TaskItem.extend({
default: false,
parseHTML: (element) => {
const checkbox = element.querySelector('input[type=checkbox].task-list-item-checkbox');
- return { checked: checkbox?.checked };
+
+ return checkbox?.checked;
},
renderHTML: (attributes) => ({
'data-checked': attributes.checked,
@@ -26,7 +28,7 @@ export default TaskItem.extend({
return [
{
tag: 'li.task-list-item',
- priority: 100,
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/task_list.js b/app/assets/javascripts/content_editor/extensions/task_list.js
index b7f6c857bc7..72c6e020102 100644
--- a/app/assets/javascripts/content_editor/extensions/task_list.js
+++ b/app/assets/javascripts/content_editor/extensions/task_list.js
@@ -1,16 +1,24 @@
import { mergeAttributes } from '@tiptap/core';
import { TaskList } from '@tiptap/extension-task-list';
+import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
+import { getMarkdownSource } from '../services/markdown_sourcemap';
export default TaskList.extend({
addAttributes() {
return {
- type: {
- default: 'ul',
- parseHTML: (element) => {
- return {
- type: element.tagName.toLowerCase() === 'ol' ? 'ol' : 'ul',
- };
- },
+ numeric: {
+ default: false,
+ parseHTML: (element) => element.tagName.toLowerCase() === 'ol',
+ },
+ start: {
+ default: 1,
+ parseHTML: (element) =>
+ element.hasAttribute('start') ? parseInt(element.getAttribute('start') || '', 10) : 1,
+ },
+
+ parens: {
+ default: false,
+ parseHTML: (element) => /^[0-9]+\)/.test(getMarkdownSource(element)),
},
};
},
@@ -19,12 +27,12 @@ export default TaskList.extend({
return [
{
tag: '.task-list',
- priority: 100,
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
},
];
},
- renderHTML({ HTMLAttributes: { type, ...HTMLAttributes } }) {
- return [type, mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
+ renderHTML({ HTMLAttributes: { numeric, ...HTMLAttributes } }) {
+ return [numeric ? 'ol' : 'ul', mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
},
});
diff --git a/app/assets/javascripts/content_editor/extensions/video.js b/app/assets/javascripts/content_editor/extensions/video.js
new file mode 100644
index 00000000000..9923b7c04cd
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/video.js
@@ -0,0 +1,10 @@
+import Playable from './playable';
+
+export default Playable.extend({
+ name: 'video',
+ defaultOptions: {
+ ...Playable.options,
+ mediaType: 'video',
+ extraElementAttrs: { width: '400' },
+ },
+});
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 8997960203a..9b2d4c9a062 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -2,19 +2,26 @@ import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment';
+import Audio from '../extensions/audio';
import Blockquote from '../extensions/blockquote';
import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
+import DescriptionItem from '../extensions/description_item';
+import DescriptionList from '../extensions/description_list';
+import Division from '../extensions/division';
import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
import Emoji from '../extensions/emoji';
+import Figure from '../extensions/figure';
+import FigureCaption from '../extensions/figure_caption';
import Gapcursor from '../extensions/gapcursor';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import History from '../extensions/history';
import HorizontalRule from '../extensions/horizontal_rule';
+import HTMLMarks from '../extensions/html_marks';
import Image from '../extensions/image';
import InlineDiff from '../extensions/inline_diff';
import Italic from '../extensions/italic';
@@ -34,6 +41,7 @@ import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
+import Video from '../extensions/video';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
@@ -62,19 +70,26 @@ export const createContentEditor = ({
const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown }),
+ Audio,
Blockquote,
Bold,
BulletList,
Code,
CodeBlockHighlight,
+ DescriptionItem,
+ DescriptionList,
Document,
+ Division,
Dropcursor,
Emoji,
+ Figure,
+ FigureCaption,
Gapcursor,
HardBreak,
Heading,
History,
HorizontalRule,
+ ...HTMLMarks,
Image,
InlineDiff,
Italic,
@@ -94,6 +109,7 @@ export const createContentEditor = ({
TaskItem,
TaskList,
Text,
+ Video,
];
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
diff --git a/app/assets/javascripts/content_editor/services/feature_flags.js b/app/assets/javascripts/content_editor/services/feature_flags.js
new file mode 100644
index 00000000000..5f7a4595938
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/feature_flags.js
@@ -0,0 +1,3 @@
+export function isBlockTablesFeatureEnabled() {
+ return gon.features?.contentEditorBlockTables;
+}
diff --git a/app/assets/javascripts/content_editor/services/mark_utils.js b/app/assets/javascripts/content_editor/services/mark_utils.js
new file mode 100644
index 00000000000..6ccfed7810a
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/mark_utils.js
@@ -0,0 +1,17 @@
+export const markInputRegex = (tag) =>
+ new RegExp(`(<(${tag})((?: \\w+=".+?")+)?>([^<]+)</${tag}>)$`, 'gm');
+
+export const extractMarkAttributesFromMatch = ([, , , attrsString]) => {
+ const attrRegex = /(\w+)="(.+?)"/g;
+ const attrs = {};
+
+ let key;
+ let value;
+
+ do {
+ [, key, value] = attrRegex.exec(attrsString) || [];
+ if (key) attrs[key] = value;
+ } while (key);
+
+ return attrs;
+};
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index df4d31c3d7f..bc6d98511f9 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -3,15 +3,22 @@ import {
defaultMarkdownSerializer,
} from 'prosemirror-markdown/src/to_markdown';
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
+import Audio from '../extensions/audio';
import Blockquote from '../extensions/blockquote';
import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
+import DescriptionItem from '../extensions/description_item';
+import DescriptionList from '../extensions/description_list';
+import Division from '../extensions/division';
import Emoji from '../extensions/emoji';
+import Figure from '../extensions/figure';
+import FigureCaption from '../extensions/figure_caption';
import HardBreak from '../extensions/hard_break';
import Heading from '../extensions/heading';
import HorizontalRule from '../extensions/horizontal_rule';
+import HTMLMarks from '../extensions/html_marks';
import Image from '../extensions/image';
import InlineDiff from '../extensions/inline_diff';
import Italic from '../extensions/italic';
@@ -30,6 +37,20 @@ import TableRow from '../extensions/table_row';
import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
+import Video from '../extensions/video';
+import {
+ isPlainURL,
+ renderHardBreak,
+ renderTable,
+ renderTableCell,
+ renderTableRow,
+ openTag,
+ closeTag,
+ renderOrderedList,
+ renderImage,
+ renderPlayable,
+ renderHTMLNode,
+} from './serialization_helpers';
const defaultSerializerConfig = {
marks: {
@@ -48,14 +69,15 @@ const defaultSerializerConfig = {
},
},
[Link.name]: {
- open() {
- return '[';
+ open(state, mark, parent, index) {
+ return isPlainURL(mark, parent, index, 1) ? '<' : '[';
},
- close(state, mark) {
+ close(state, mark, parent, index) {
const href = mark.attrs.canonicalSrc || mark.attrs.href;
- return `](${state.esc(href)}${
- mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''
- })`;
+
+ return isPlainURL(mark, parent, index, -1)
+ ? '>'
+ : `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
},
},
[Strike.name]: {
@@ -64,9 +86,35 @@ const defaultSerializerConfig = {
mixable: true,
expelEnclosingWhitespace: true,
},
+ ...HTMLMarks.reduce(
+ (acc, { name }) => ({
+ ...acc,
+ [name]: {
+ mixable: true,
+ open(state, node) {
+ return openTag(name, node.attrs);
+ },
+ close: closeTag(name),
+ },
+ }),
+ {},
+ ),
},
+
nodes: {
- [Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
+ [Audio.name]: renderPlayable,
+ [Blockquote.name]: (state, node) => {
+ if (node.attrs.multiline) {
+ state.write('>>>');
+ state.ensureNewLine();
+ state.renderContent(node);
+ state.ensureNewLine();
+ state.write('>>>');
+ state.closeBlock(node);
+ } else {
+ state.wrapBlock('> ', null, node, () => state.renderContent(node));
+ }
+ },
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
[CodeBlockHighlight.name]: (state, node) => {
state.write(`\`\`\`${node.attrs.language || ''}\n`);
@@ -75,94 +123,47 @@ const defaultSerializerConfig = {
state.write('```');
state.closeBlock(node);
},
+ [Division.name]: renderHTMLNode('div'),
+ [DescriptionList.name]: renderHTMLNode('dl', true),
+ [DescriptionItem.name]: (state, node, parent, index) => {
+ if (index === 1) state.ensureNewLine();
+ renderHTMLNode(node.attrs.isTerm ? 'dt' : 'dd')(state, node);
+ if (index === parent.childCount - 1) state.ensureNewLine();
+ },
[Emoji.name]: (state, node) => {
const { name } = node.attrs;
state.write(`:${name}:`);
},
- [HardBreak.name]: defaultMarkdownSerializer.nodes.hard_break,
+ [Figure.name]: renderHTMLNode('figure'),
+ [FigureCaption.name]: renderHTMLNode('figcaption'),
+ [HardBreak.name]: renderHardBreak,
[Heading.name]: defaultMarkdownSerializer.nodes.heading,
[HorizontalRule.name]: defaultMarkdownSerializer.nodes.horizontal_rule,
- [Image.name]: (state, node) => {
- const { alt, canonicalSrc, src, title } = node.attrs;
- const quotedTitle = title ? ` ${state.quote(title)}` : '';
-
- state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
- },
+ [Image.name]: renderImage,
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
- [OrderedList.name]: defaultMarkdownSerializer.nodes.ordered_list,
+ [OrderedList.name]: renderOrderedList,
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
[Reference.name]: (state, node) => {
state.write(node.attrs.originalText || node.attrs.text);
},
- [Table.name]: (state, node) => {
- state.renderContent(node);
- },
- [TableCell.name]: (state, node) => {
- state.renderInline(node);
- },
- [TableHeader.name]: (state, node) => {
- state.renderInline(node);
- },
- [TableRow.name]: (state, node) => {
- const isHeaderRow = node.child(0).type.name === 'tableHeader';
-
- const renderRow = () => {
- const cellWidths = [];
-
- state.flushClose(1);
-
- state.write('| ');
- node.forEach((cell, _, i) => {
- if (i) state.write(' | ');
-
- const { length } = state.out;
- state.render(cell, node, i);
- cellWidths.push(state.out.length - length);
- });
- state.write(' |');
-
- state.closeBlock(node);
-
- return cellWidths;
- };
-
- const renderHeaderRow = (cellWidths) => {
- state.flushClose(1);
-
- state.write('|');
- node.forEach((cell, _, i) => {
- if (i) state.write('|');
-
- state.write(cell.attrs.align === 'center' ? ':' : '-');
- state.write(state.repeat('-', cellWidths[i]));
- state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
- });
- state.write('|');
-
- state.closeBlock(node);
- };
-
- if (isHeaderRow) {
- renderHeaderRow(renderRow());
- } else {
- renderRow();
- }
- },
+ [Table.name]: renderTable,
+ [TableCell.name]: renderTableCell,
+ [TableHeader.name]: renderTableCell,
+ [TableRow.name]: renderTableRow,
[TaskItem.name]: (state, node) => {
state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
state.renderContent(node);
},
[TaskList.name]: (state, node) => {
- if (node.attrs.type === 'ul') defaultMarkdownSerializer.nodes.bullet_list(state, node);
- else defaultMarkdownSerializer.nodes.ordered_list(state, node);
+ if (node.attrs.numeric) renderOrderedList(state, node);
+ else defaultMarkdownSerializer.nodes.bullet_list(state, node);
},
[Text.name]: defaultMarkdownSerializer.nodes.text,
+ [Video.name]: renderPlayable,
},
};
-const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
-
/**
* A markdown serializer converts arbitrary Markdown content
* into a ProseMirror document and viceversa. To convert Markdown
@@ -175,7 +176,7 @@ const wrapHtmlPayload = (payload) => `<div>${payload}</div>`;
* that parses the Markdown and converts it into HTML.
* @returns a markdown serializer
*/
-export default ({ render = () => null, serializerConfig }) => ({
+export default ({ render = () => null, serializerConfig = {} } = {}) => ({
/**
* Converts a Markdown string into a ProseMirror JSONDocument based
* on a ProseMirror schema.
@@ -187,15 +188,15 @@ export default ({ render = () => null, serializerConfig }) => ({
deserialize: async ({ schema, content }) => {
const html = await render(content);
- if (!html) {
- return null;
- }
+ if (!html) return null;
const parser = new DOMParser();
- const {
- body: { firstElementChild },
- } = parser.parseFromString(wrapHtmlPayload(html), 'text/html');
- const state = ProseMirrorDOMParser.fromSchema(schema).parse(firstElementChild);
+ const { body } = parser.parseFromString(html, 'text/html');
+
+ // append original source as a comment that nodes can access
+ body.append(document.createComment(content));
+
+ const state = ProseMirrorDOMParser.fromSchema(schema).parse(body);
return state.toJSON();
},
diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
new file mode 100644
index 00000000000..a1199589c9b
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
@@ -0,0 +1,40 @@
+const getFullSource = (element) => {
+ const commentNode = element.ownerDocument.body.lastChild;
+
+ if (commentNode.nodeName === '#comment') {
+ return commentNode.textContent.split('\n');
+ }
+
+ return [];
+};
+
+const getRangeFromSourcePos = (sourcePos) => {
+ const [start, end] = sourcePos.split('-');
+ const [startRow, startCol] = start.split(':');
+ const [endRow, endCol] = end.split(':');
+
+ return {
+ start: { row: Number(startRow) - 1, col: Number(startCol) - 1 },
+ end: { row: Number(endRow) - 1, col: Number(endCol) - 1 },
+ };
+};
+
+export const getMarkdownSource = (element) => {
+ if (!element.dataset.sourcepos) return undefined;
+
+ const source = getFullSource(element);
+ const range = getRangeFromSourcePos(element.dataset.sourcepos);
+ let elSource = '';
+
+ for (let i = range.start.row; i <= range.end.row; i += 1) {
+ if (i === range.start.row) {
+ elSource += source[i]?.substring(range.start.col);
+ } else if (i === range.end.row) {
+ elSource += `\n${source[i]?.substring(0, range.start.col)}`;
+ } else {
+ elSource += `\n${source[i]}` || '';
+ }
+ }
+
+ return elSource.trim();
+};
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
new file mode 100644
index 00000000000..b2327555b45
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -0,0 +1,345 @@
+import { uniq } from 'lodash';
+import { isBlockTablesFeatureEnabled } from './feature_flags';
+
+const defaultAttrs = {
+ td: { colspan: 1, rowspan: 1, colwidth: null },
+ th: { colspan: 1, rowspan: 1, colwidth: null },
+};
+
+const ignoreAttrs = {
+ dd: ['isTerm'],
+ dt: ['isTerm'],
+};
+
+const tableMap = new WeakMap();
+
+// Source taken from
+// prosemirror-markdown/src/to_markdown.js
+export function isPlainURL(link, parent, index, side) {
+ if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
+ const content = parent.child(index + (side < 0 ? -1 : 0));
+ if (
+ !content.isText ||
+ content.text !== link.attrs.href ||
+ content.marks[content.marks.length - 1] !== link
+ )
+ return false;
+ if (index === (side < 0 ? 1 : parent.childCount - 1)) return true;
+ const next = parent.child(index + (side < 0 ? -2 : 1));
+ return !link.isInSet(next.marks);
+}
+
+function containsOnlyText(node) {
+ if (node.childCount === 1) {
+ const child = node.child(0);
+ return child.isText && child.marks.length === 0;
+ }
+
+ return false;
+}
+
+function containsParagraphWithOnlyText(cell) {
+ if (cell.childCount === 1) {
+ const child = cell.child(0);
+ if (child.type.name === 'paragraph') {
+ return containsOnlyText(child);
+ }
+ }
+
+ return false;
+}
+
+function getRowsAndCells(table) {
+ const cells = [];
+ const rows = [];
+ table.descendants((n) => {
+ if (n.type.name === 'tableCell' || n.type.name === 'tableHeader') {
+ cells.push(n);
+ return false;
+ }
+
+ if (n.type.name === 'tableRow') {
+ rows.push(n);
+ }
+
+ return true;
+ });
+ return { rows, cells };
+}
+
+function getChildren(node) {
+ const children = [];
+ for (let i = 0; i < node.childCount; i += 1) {
+ children.push(node.child(i));
+ }
+ return children;
+}
+
+function shouldRenderHTMLTable(table) {
+ const { rows, cells } = getRowsAndCells(table);
+
+ const cellChildCount = Math.max(...cells.map((cell) => cell.childCount));
+ const maxColspan = Math.max(...cells.map((cell) => cell.attrs.colspan));
+ const maxRowspan = Math.max(...cells.map((cell) => cell.attrs.rowspan));
+
+ const rowChildren = rows.map((row) => uniq(getChildren(row).map((cell) => cell.type.name)));
+ const cellTypeInFirstRow = rowChildren[0];
+ const cellTypesInOtherRows = uniq(rowChildren.slice(1).map(([type]) => type));
+
+ // if the first row has headers, and there are no headers anywhere else, render markdown table
+ if (
+ !(
+ cellTypeInFirstRow.length === 1 &&
+ cellTypeInFirstRow[0] === 'tableHeader' &&
+ cellTypesInOtherRows.length === 1 &&
+ cellTypesInOtherRows[0] === 'tableCell'
+ )
+ ) {
+ return true;
+ }
+
+ if (cellChildCount === 1 && maxColspan === 1 && maxRowspan === 1) {
+ // if all rows contain only one paragraph each and no rowspan/colspan, render markdown table
+ const children = uniq(cells.map((cell) => cell.child(0).type.name));
+ if (children.length === 1 && children[0] === 'paragraph') {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function htmlEncode(str = '') {
+ return str
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/'/g, '&#39;')
+ .replace(/"/g, '&#34;');
+}
+
+export function openTag(tagName, attrs) {
+ let str = `<${tagName}`;
+
+ str += Object.entries(attrs || {})
+ .map(([key, value]) => {
+ if ((ignoreAttrs[tagName] || []).includes(key) || defaultAttrs[tagName]?.[key] === value)
+ return '';
+
+ return ` ${key}="${htmlEncode(value?.toString())}"`;
+ })
+ .join('');
+
+ return `${str}>`;
+}
+
+export function closeTag(tagName) {
+ return `</${tagName}>`;
+}
+
+function isInBlockTable(node) {
+ return tableMap.get(node);
+}
+
+function isInTable(node) {
+ return tableMap.has(node);
+}
+
+function setIsInBlockTable(table, value) {
+ tableMap.set(table, value);
+
+ const { rows, cells } = getRowsAndCells(table);
+ rows.forEach((row) => tableMap.set(row, value));
+ cells.forEach((cell) => {
+ tableMap.set(cell, value);
+ if (cell.childCount && cell.child(0).type.name === 'paragraph')
+ tableMap.set(cell.child(0), value);
+ });
+}
+
+function unsetIsInBlockTable(table) {
+ tableMap.delete(table);
+
+ const { rows, cells } = getRowsAndCells(table);
+ rows.forEach((row) => tableMap.delete(row));
+ cells.forEach((cell) => {
+ tableMap.delete(cell);
+ if (cell.childCount) tableMap.delete(cell.child(0));
+ });
+}
+
+function renderTagOpen(state, tagName, attrs) {
+ state.ensureNewLine();
+ state.write(openTag(tagName, attrs));
+}
+
+function renderTagClose(state, tagName, insertNewline = true) {
+ state.write(closeTag(tagName));
+ if (insertNewline) state.ensureNewLine();
+}
+
+function renderTableHeaderRowAsMarkdown(state, node, cellWidths) {
+ state.flushClose(1);
+
+ state.write('|');
+ node.forEach((cell, _, i) => {
+ if (i) state.write('|');
+
+ state.write(cell.attrs.align === 'center' ? ':' : '-');
+ state.write(state.repeat('-', cellWidths[i]));
+ state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
+ });
+ state.write('|');
+
+ state.closeBlock(node);
+}
+
+function renderTableRowAsMarkdown(state, node, isHeaderRow = false) {
+ const cellWidths = [];
+
+ state.flushClose(1);
+
+ state.write('| ');
+ node.forEach((cell, _, i) => {
+ if (i) state.write(' | ');
+
+ const { length } = state.out;
+ state.render(cell, node, i);
+ cellWidths.push(state.out.length - length);
+ });
+ state.write(' |');
+
+ state.closeBlock(node);
+
+ if (isHeaderRow) renderTableHeaderRowAsMarkdown(state, node, cellWidths);
+}
+
+function renderTableRowAsHTML(state, node) {
+ renderTagOpen(state, 'tr');
+
+ node.forEach((cell, _, i) => {
+ const tag = cell.type.name === 'tableHeader' ? 'th' : 'td';
+
+ renderTagOpen(state, tag, cell.attrs);
+
+ if (!containsParagraphWithOnlyText(cell)) {
+ state.closeBlock(node);
+ state.flushClose();
+ }
+
+ state.render(cell, node, i);
+ state.flushClose(1);
+
+ renderTagClose(state, tag);
+ });
+
+ renderTagClose(state, 'tr');
+}
+
+export function renderContent(state, node, forceRenderInline) {
+ if (node.type.inlineContent) {
+ if (containsOnlyText(node)) {
+ state.renderInline(node);
+ } else {
+ state.closeBlock(node);
+ state.flushClose();
+ state.renderInline(node);
+ state.closeBlock(node);
+ state.flushClose();
+ }
+ } else {
+ const renderInline = forceRenderInline || containsParagraphWithOnlyText(node);
+ if (!renderInline) {
+ state.closeBlock(node);
+ state.flushClose();
+ state.renderContent(node);
+ state.ensureNewLine();
+ } else {
+ state.renderInline(forceRenderInline ? node : node.child(0));
+ }
+ }
+}
+
+export function renderHTMLNode(tagName, forceRenderInline = false) {
+ return (state, node) => {
+ renderTagOpen(state, tagName, node.attrs);
+ renderContent(state, node, forceRenderInline);
+ renderTagClose(state, tagName, false);
+ };
+}
+
+export function renderOrderedList(state, node) {
+ const { parens } = node.attrs;
+ const start = node.attrs.start || 1;
+ const maxW = String(start + node.childCount - 1).length;
+ const space = state.repeat(' ', maxW + 2);
+ const delimiter = parens ? ')' : '.';
+
+ state.renderList(node, space, (i) => {
+ const nStr = String(start + i);
+ return `${state.repeat(' ', maxW - nStr.length) + nStr}${delimiter} `;
+ });
+}
+
+export function renderTableCell(state, node) {
+ if (!isBlockTablesFeatureEnabled()) {
+ state.renderInline(node);
+ return;
+ }
+
+ if (!isInBlockTable(node) || containsParagraphWithOnlyText(node)) {
+ state.renderInline(node.child(0));
+ } else {
+ state.renderContent(node);
+ }
+}
+
+export function renderTableRow(state, node) {
+ if (isInBlockTable(node)) {
+ renderTableRowAsHTML(state, node);
+ } else {
+ renderTableRowAsMarkdown(state, node, node.child(0).type.name === 'tableHeader');
+ }
+}
+
+export function renderTable(state, node) {
+ if (isBlockTablesFeatureEnabled()) {
+ setIsInBlockTable(node, shouldRenderHTMLTable(node));
+ }
+
+ if (isInBlockTable(node)) renderTagOpen(state, 'table');
+
+ state.renderContent(node);
+
+ if (isInBlockTable(node)) renderTagClose(state, 'table');
+
+ // ensure at least one blank line after any table
+ state.closeBlock(node);
+ state.flushClose();
+
+ if (isBlockTablesFeatureEnabled()) {
+ unsetIsInBlockTable(node);
+ }
+}
+
+export function renderHardBreak(state, node, parent, index) {
+ const br = isInTable(parent) ? '<br>' : '\\\n';
+
+ for (let i = index + 1; i < parent.childCount; i += 1) {
+ if (parent.child(i).type !== node.type) {
+ state.write(br);
+ return;
+ }
+ }
+}
+
+export function renderImage(state, node) {
+ const { alt, canonicalSrc, src, title } = node.attrs;
+ const quotedTitle = title ? ` ${state.quote(title)}` : '';
+
+ state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
+}
+
+export function renderPlayable(state, node) {
+ renderImage(state, node);
+}
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
index 45c886978f1..004c2e26c4e 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
+++ b/app/assets/javascripts/create_cluster/eks_cluster/components/service_credentials_form.vue
@@ -1,9 +1,7 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButton, GlFormGroup, GlFormInput, GlIcon, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
-import { escape } from 'lodash';
import { mapState, mapActions } from 'vuex';
-import { sprintf, s__, __ } from '~/locale';
+import { s__, __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { DEFAULT_REGION } from '../constants';
@@ -38,6 +36,9 @@ export default {
regionHelpText: s__(
'ClusterIntegration|Select the region you want to create the new cluster in. Make sure you have access to this region for your role to be able to authenticate. If no region is selected, we will use %{codeStart}DEFAULT_REGION%{codeEnd}. Learn more about %{linkStart}Regions%{linkEnd}.',
),
+ accountAndExternalIdsHelpText: s__(
+ 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{awsLinkStart}Amazon Web Services %{awsLinkEnd} using the above account and external IDs. %{moreInfoStart}More information%{moreInfoEnd}',
+ ),
regionHelpTextDefaultRegion: DEFAULT_REGION,
},
data() {
@@ -56,39 +57,8 @@ export default {
? __('Authenticating')
: s__('ClusterIntegration|Authenticate with AWS');
},
- accountAndExternalIdsHelpText() {
- const escapedUrl = escape(this.accountAndExternalIdsHelpPath);
-
- return sprintf(
- s__(
- 'ClusterIntegration|Create a provision role on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the account and external ID above. %{startMoreInfoLink}More information%{endLink}',
- ),
- {
- startAwsLink:
- '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
- startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
- externalLinkIcon: this.externalLinkIcon,
- endLink: '</a>',
- },
- false,
- );
- },
- provisionRoleArnHelpText() {
- const escapedUrl = escape(this.createRoleArnHelpPath);
-
- return sprintf(
- s__(
- 'ClusterIntegration|The Amazon Resource Name (ARN) associated with your role. If you do not have a provisioned role, first create one on %{startAwsLink}Amazon Web Services %{externalLinkIcon}%{endLink} using the above account and external IDs. %{startMoreInfoLink}More information%{endLink}',
- ),
- {
- startAwsLink:
- '<a href="https://console.aws.amazon.com/iam/home?#roles" target="_blank" rel="noopener noreferrer">',
- startMoreInfoLink: `<a href="${escapedUrl}" target="_blank" rel="noopener noreferrer">`,
- externalLinkIcon: this.externalLinkIcon,
- endLink: '</a>',
- },
- false,
- );
+ awsHelpLink() {
+ return 'https://console.aws.amazon.com/iam/home?#roles';
},
},
methods: {
@@ -142,13 +112,41 @@ export default {
</div>
</div>
<div class="col-12 mb-3 mt-n3">
- <p class="form-text text-muted" v-html="accountAndExternalIdsHelpText"></p>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
+ <template #awsLink="{ content }">
+ <gl-link :href="awsHelpLink" target="_blank">
+ {{ content }}
+ <gl-icon name="external-link" class="gl-vertical-align-middle" />
+ </gl-link>
+ </template>
+ <template #moreInfo="{ content }">
+ <gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
</div>
</div>
<div class="form-group">
<label for="eks-provision-role-arn">{{ s__('ClusterIntegration|Provision Role ARN') }}</label>
<gl-form-input id="eks-provision-role-arn" v-model="roleArn" />
- <p class="form-text text-muted" v-html="provisionRoleArnHelpText"></p>
+ <p class="form-text text-muted">
+ <gl-sprintf :message="$options.i18n.accountAndExternalIdsHelpText">
+ <template #awsLink="{ content }">
+ <gl-link :href="awsHelpLink" target="_blank">
+ {{ content }}
+ <gl-icon name="external-link" class="gl-vertical-align-middle" />
+ </gl-link>
+ </template>
+ <template #moreInfo="{ content }">
+ <gl-link :href="accountAndExternalIdsHelpPath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
</div>
<gl-form-group :label="$options.i18n.regionInputLabel">
diff --git a/app/assets/javascripts/create_cluster/eks_cluster/constants.js b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
index 1c698cc2796..3ed0f050301 100644
--- a/app/assets/javascripts/create_cluster/eks_cluster/constants.js
+++ b/app/assets/javascripts/create_cluster/eks_cluster/constants.js
@@ -1,9 +1,9 @@
export const DEFAULT_REGION = 'us-east-2';
export const KUBERNETES_VERSIONS = [
- { name: '1.15', value: '1.15' },
{ name: '1.16', value: '1.16' },
{ name: '1.17', value: '1.17' },
{ name: '1.18', value: '1.18' },
- { name: '1.19', value: '1.19', default: true },
+ { name: '1.19', value: '1.19' },
+ { name: '1.20', value: '1.20', default: true },
];
diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue
deleted file mode 100644
index cf4c35ef12b..00000000000
--- a/app/assets/javascripts/cycle_analytics/components/banner.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script>
-/* eslint-disable vue/no-v-html */
-import { GlIcon } from '@gitlab/ui';
-import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
-
-export default {
- components: {
- GlIcon,
- },
- props: {
- documentationLink: {
- type: String,
- required: true,
- },
- },
- computed: {
- iconCycleAnalyticsSplash() {
- return iconCycleAnalyticsSplash;
- },
- },
- methods: {
- dismissOverviewDialog() {
- this.$emit('dismiss-overview-dialog');
- },
- },
-};
-</script>
-<template>
- <div class="landing content-block">
- <button
- :aria-label="__('Dismiss Value Stream Analytics introduction box')"
- class="js-ca-dismiss-button dismiss-button"
- type="button"
- @click="dismissOverviewDialog"
- >
- <gl-icon name="close" />
- </button>
- <div class="svg-container" v-html="iconCycleAnalyticsSplash"></div>
- <div class="inner-content">
- <h4>{{ __('Introducing Value Stream Analytics') }}</h4>
- <p>
- {{
- __(`Value Stream Analytics gives an overview
-of how much time it takes to go from idea to production in your project.`)
- }}
- </p>
- <p>
- <a :href="documentationLink" target="_blank" rel="nofollow" class="btn">
- {{ __('Read more') }}
- </a>
- </p>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index c9ecac6829b..ae78ce33263 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -1,9 +1,10 @@
<script>
-import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
+import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
@@ -13,11 +14,10 @@ const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
export default {
name: 'CycleAnalytics',
components: {
- GlIcon,
GlLoadingIcon,
- GlSprintf,
PathNavigation,
StageTable,
+ ValueStreamFilters,
ValueStreamMetrics,
},
props: {
@@ -45,11 +45,12 @@ export default {
'selectedStageError',
'stages',
'summary',
- 'daysInPast',
'permissions',
'stageCounts',
'endpoints',
'features',
+ 'createdBefore',
+ 'createdAfter',
]),
...mapGetters(['pathNavigationData', 'filterParams']),
displayStageEvents() {
@@ -98,14 +99,12 @@ export default {
},
},
methods: {
- ...mapActions([
- 'fetchCycleAnalyticsData',
- 'fetchStageData',
- 'setSelectedStage',
- 'setDateRange',
- ]),
- handleDateSelect(daysInPast) {
- this.setDateRange(daysInPast);
+ ...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']),
+ onSetDateRange({ startDate, endDate }) {
+ this.setDateRange({
+ createdAfter: new Date(startDate),
+ createdBefore: new Date(endDate),
+ });
},
onSelectStage(stage) {
this.setSelectedStage(stage);
@@ -133,35 +132,22 @@ export default {
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
<path-navigation
v-if="displayPathNavigation"
- class="js-path-navigation gl-w-full gl-pb-2"
+ data-testid="vsa-path-navigation"
+ class="gl-w-full gl-pb-2"
:loading="isLoading || isLoadingStage"
:stages="pathNavigationData"
:selected-stage="selectedStage"
@selected="onSelectStage"
/>
- <div class="gl-flex-grow gl-align-self-end">
- <div class="js-ca-dropdown dropdown inline">
- <!-- eslint-disable-next-line @gitlab/vue-no-data-toggle -->
- <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button">
- <span class="dropdown-label">
- <gl-sprintf :message="$options.i18n.dropdownText">
- <template #days>{{ daysInPast }}</template>
- </gl-sprintf>
- <gl-icon name="chevron-down" class="dropdown-menu-toggle-icon gl-top-3" />
- </span>
- </button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li v-for="days in $options.dayRangeOptions" :key="`day-range-${days}`">
- <a href="#" @click.prevent="handleDateSelect(days)">
- <gl-sprintf :message="$options.i18n.dropdownText">
- <template #days>{{ days }}</template>
- </gl-sprintf>
- </a>
- </li>
- </ul>
- </div>
- </div>
</div>
+ <value-stream-filters
+ :group-id="endpoints.groupId"
+ :group-path="endpoints.groupPath"
+ :has-project-filter="false"
+ :start-date="createdAfter"
+ :end-date="createdBefore"
+ @setDateRange="onSetDateRange"
+ />
<value-stream-metrics
:request-path="endpoints.fullPath"
:request-params="filterParams"
@@ -178,6 +164,7 @@ export default {
:empty-state-message="emptyStageText"
:no-data-svg-path="noDataSvgPath"
:pagination="null"
+ :sortable="false"
/>
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
index 0c47838c773..8a2667a4ab1 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -23,8 +23,8 @@ import TotalTime from './total_time_component.vue';
const DEFAULT_WORKFLOW_TITLE_PROPERTIES = {
thClass: 'gl-w-half',
key: PAGINATION_SORT_FIELD_END_EVENT,
- sortable: true,
};
+
const WORKFLOW_COLUMN_TITLES = {
issues: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Issues') },
jobs: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Jobs') },
@@ -84,6 +84,11 @@ export default {
required: false,
default: null,
},
+ sortable: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
if (this.pagination) {
@@ -122,9 +127,11 @@ export default {
key: PAGINATION_SORT_FIELD_DURATION,
label: __('Time'),
thClass: 'gl-w-half',
- sortable: true,
},
- ];
+ ].map((field) => ({
+ ...field,
+ sortable: this.sortable,
+ }));
},
prevPage() {
return Math.max(this.pagination.page - 1, 0);
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
index 6b1e537dc77..8610dfc2b03 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
@@ -61,33 +61,38 @@ export default {
<template>
<div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom">
<filter-bar
- class="js-filter-bar filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
+ data-testid="vsa-filter-bar"
+ class="filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
:group-path="groupPath"
/>
<div
v-if="hasDateRangeFilter || hasProjectFilter"
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
>
- <projects-dropdown-filter
- v-if="hasProjectFilter"
- :key="groupId"
- class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
- :group-id="groupId"
- :group-namespace="groupPath"
- :query-params="projectsQueryParams"
- :multi-select="$options.multiProjectSelect"
- :default-projects="selectedProjects"
- @selected="$emit('selectProject', $event)"
- />
- <date-range
- v-if="hasDateRangeFilter"
- :start-date="startDate"
- :end-date="endDate"
- :max-date-range="$options.maxDateRange"
- :include-selected-date="true"
- class="js-daterange-picker"
- @change="$emit('setDateRange', $event)"
- />
+ <div>
+ <projects-dropdown-filter
+ v-if="hasProjectFilter"
+ :key="groupId"
+ class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
+ :group-id="groupId"
+ :group-namespace="groupPath"
+ :query-params="projectsQueryParams"
+ :multi-select="$options.multiProjectSelect"
+ :default-projects="selectedProjects"
+ @selected="$emit('selectProject', $event)"
+ />
+ </div>
+ <div>
+ <date-range
+ v-if="hasDateRangeFilter"
+ :start-date="startDate"
+ :end-date="endDate"
+ :max-date-range="$options.maxDateRange"
+ :include-selected-date="true"
+ class="js-daterange-picker"
+ @change="$emit('setDateRange', $event)"
+ />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 3827db4d9b2..620da0104e0 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -1,7 +1,9 @@
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import CycleAnalytics from './components/base.vue';
+import { DEFAULT_DAYS_TO_DISPLAY } from './constants';
import createStore from './store';
+import { calculateFormattedDayInPast } from './utils';
Vue.use(Translate);
@@ -14,19 +16,29 @@ export default () => {
requestPath,
fullPath,
projectId,
+ groupId,
groupPath,
+ labelsPath,
+ milestonesPath,
} = el.dataset;
+ const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+
store.dispatch('initializeVsa', {
projectId: parseInt(projectId, 10),
- groupPath,
endpoints: {
requestPath,
fullPath,
+ labelsPath,
+ milestonesPath,
+ groupId: parseInt(groupId, 10),
+ groupPath,
},
features: {
cycleAnalyticsForGroups: Boolean(gon?.licensed_features?.cycleAnalyticsForGroups),
},
+ createdBefore: new Date(now),
+ createdAfter: new Date(past),
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index a7a2c8ea9d3..e39cd224199 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -163,6 +163,7 @@ const refetchStageData = (dispatch) => {
dispatch('fetchCycleAnalyticsData'),
dispatch('fetchStageData'),
dispatch('fetchStageMedians'),
+ dispatch('fetchStageCountValues'),
]),
)
.finally(() => dispatch('setLoading', false));
@@ -170,14 +171,24 @@ const refetchStageData = (dispatch) => {
export const setFilters = ({ dispatch }) => refetchStageData(dispatch);
-export const setDateRange = ({ dispatch, commit }, daysInPast) => {
- commit(types.SET_DATE_RANGE, daysInPast);
+export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore }) => {
+ commit(types.SET_DATE_RANGE, { createdAfter, createdBefore });
return refetchStageData(dispatch);
};
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData);
+ const {
+ endpoints: { fullPath, groupPath, milestonesPath = '', labelsPath = '' },
+ } = initialData;
+ dispatch('filters/setEndpoints', {
+ labelsEndpoint: labelsPath,
+ milestonesEndpoint: milestonesPath,
+ groupEndpoint: groupPath,
+ projectEndpoint: fullPath,
+ });
+
return dispatch('setLoading', true)
.then(() => dispatch('fetchValueStreams'))
.finally(() => dispatch('setLoading', false));
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js
index 9faccabcaad..77c285f5ce0 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/cycle_analytics/store/getters.js
@@ -1,5 +1,6 @@
import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants';
+import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
@@ -20,6 +21,21 @@ export const requestParams = (state) => {
return { requestPath: fullPath, valueStreamId, stageId };
};
+const filterBarParams = ({ filters }) => {
+ const {
+ authors: { selected: selectedAuthor },
+ milestones: { selected: selectedMilestone },
+ assignees: { selectedList: selectedAssigneeList },
+ labels: { selectedList: selectedLabelList },
+ } = filters;
+ return filterToQueryObject({
+ milestone_title: selectedMilestone,
+ author_username: selectedAuthor,
+ label_name: selectedLabelList,
+ assignee_username: selectedAssigneeList,
+ });
+};
+
const dateRangeParams = ({ createdAfter, createdBefore }) => ({
created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null,
created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
@@ -33,6 +49,7 @@ export const legacyFilterParams = ({ daysInPast }) => {
export const filterParams = (state) => {
return {
+ ...filterBarParams(state),
...dateRangeParams(state),
};
};
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index e41de85c1fa..301e7d95f8c 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -1,14 +1,12 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
-import { formatMedianValues, calculateFormattedDayInPast } from '../utils';
+import { formatMedianValues } from '../utils';
import * as types from './mutation_types';
export default {
- [types.INITIALIZE_VSA](state, { endpoints, features }) {
+ [types.INITIALIZE_VSA](state, { endpoints, features, createdBefore, createdAfter }) {
state.endpoints = endpoints;
- const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
- state.createdBefore = now;
- state.createdAfter = past;
+ state.createdBefore = createdBefore;
+ state.createdAfter = createdAfter;
state.features = features;
},
[types.SET_LOADING](state, loadingState) {
@@ -20,11 +18,9 @@ export default {
[types.SET_SELECTED_STAGE](state, stage) {
state.selectedStage = stage;
},
- [types.SET_DATE_RANGE](state, daysInPast) {
- state.daysInPast = daysInPast;
- const { now, past } = calculateFormattedDayInPast(daysInPast);
- state.createdBefore = now;
- state.createdAfter = past;
+ [types.SET_DATE_RANGE](state, { createdAfter, createdBefore }) {
+ state.createdBefore = createdBefore;
+ state.createdAfter = createdAfter;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js
index e6da3f609b2..0882db51218 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/cycle_analytics/store/state.js
@@ -1,10 +1,7 @@
-import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
-
export default () => ({
id: null,
features: {},
endpoints: {},
- daysInPast: DEFAULT_DAYS_TO_DISPLAY,
createdAfter: null,
createdBefore: null,
stages: [],
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
index 8282f1d910a..77767456f76 100644
--- a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
+++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTable, GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { GlTable, GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
@@ -21,21 +21,42 @@ export default {
key: 'edit',
label: s__('DeployFreeze|Edit'),
},
+ {
+ key: 'delete',
+ label: s__('DeployFreeze|Delete'),
+ },
],
translations: {
addDeployFreeze: s__('DeployFreeze|Add deploy freeze'),
+ deleteDeployFreezeTitle: s__('DeployFreeze|Delete deploy freeze?'),
+ deleteDeployFreezeMessage: s__(
+ 'DeployFreeze|Deploy freeze from %{start} to %{end} in %{timezone} will be removed. Are you sure?',
+ ),
emptyStateText: s__(
'DeployFreeze|No deploy freezes exist for this project. To add one, select %{strongStart}Add deploy freeze%{strongEnd}',
),
},
+ modal: {
+ id: 'deleteFreezePeriodModal',
+ actionPrimary: {
+ text: s__('DeployFreeze|Delete freeze period'),
+ attributes: { variant: 'danger', 'data-testid': 'modal-confirm' },
+ },
+ },
components: {
GlTable,
GlButton,
+ GlModal,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
+ data() {
+ return {
+ freezePeriodToDelete: null,
+ };
+ },
computed: {
...mapState(['freezePeriods']),
tableIsNotEmpty() {
@@ -46,7 +67,14 @@ export default {
this.fetchFreezePeriods();
},
methods: {
- ...mapActions(['fetchFreezePeriods', 'setFreezePeriod']),
+ ...mapActions(['fetchFreezePeriods', 'setFreezePeriod', 'deleteFreezePeriod']),
+ handleDeleteFreezePeriod(freezePeriod) {
+ this.freezePeriodToDelete = freezePeriod;
+ },
+ confirmDeleteFreezePeriod() {
+ this.deleteFreezePeriod(this.freezePeriodToDelete);
+ this.freezePeriodToDelete = null;
+ },
},
};
</script>
@@ -72,6 +100,18 @@ export default {
@click="setFreezePeriod(item)"
/>
</template>
+ <template #cell(delete)="{ item }">
+ <gl-button
+ v-gl-modal="$options.modal.id"
+ category="secondary"
+ variant="danger"
+ icon="remove"
+ :aria-label="$options.modal.actionPrimary.text"
+ :loading="item.isDeleting"
+ data-testid="delete-deploy-freeze"
+ @click="handleDeleteFreezePeriod(item)"
+ />
+ </template>
<template #empty>
<p data-testid="empty-freeze-periods" class="gl-text-center text-plain">
<gl-sprintf :message="$options.translations.emptyStateText">
@@ -90,5 +130,24 @@ export default {
>
{{ $options.translations.addDeployFreeze }}
</gl-button>
+ <gl-modal
+ :title="$options.translations.deleteDeployFreezeTitle"
+ :modal-id="$options.modal.id"
+ :action-primary="$options.modal.actionPrimary"
+ static
+ @primary="confirmDeleteFreezePeriod"
+ >
+ <template v-if="freezePeriodToDelete">
+ <gl-sprintf :message="$options.translations.deleteDeployFreezeMessage">
+ <template #start>
+ <code>{{ freezePeriodToDelete.freezeStart }}</code>
+ </template>
+ <template #end>
+ <code>{{ freezePeriodToDelete.freezeEnd }}</code>
+ </template>
+ <template #timezone>{{ freezePeriodToDelete.cronTimezone.formattedTimezone }}</template>
+ </gl-sprintf>
+ </template>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js
index fed80b46eda..1ac6781a0e3 100644
--- a/app/assets/javascripts/deploy_freeze/store/actions.js
+++ b/app/assets/javascripts/deploy_freeze/store/actions.js
@@ -1,5 +1,6 @@
import Api from '~/api';
import createFlash from '~/flash';
+import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -52,6 +53,21 @@ export const updateFreezePeriod = (store) =>
}),
);
+export const deleteFreezePeriod = ({ state, commit }, { id }) => {
+ commit(types.REQUEST_DELETE_FREEZE_PERIOD, id);
+
+ return Api.deleteFreezePeriod(state.projectId, id)
+ .then(() => commit(types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS, id))
+ .catch((e) => {
+ createFlash({
+ message: __('Error: Unable to delete deploy freeze'),
+ });
+ commit(types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR, id);
+
+ logError(`Unable to delete deploy freeze`, e);
+ });
+};
+
export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.REQUEST_FREEZE_PERIODS);
diff --git a/app/assets/javascripts/deploy_freeze/store/mutation_types.js b/app/assets/javascripts/deploy_freeze/store/mutation_types.js
index 8e6fdfd4443..0fec96e2e4c 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutation_types.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutation_types.js
@@ -10,4 +10,8 @@ export const SET_SELECTED_ID = 'SET_SELECTED_ID';
export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON';
export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON';
+export const REQUEST_DELETE_FREEZE_PERIOD = 'REQUEST_DELETE_FREEZE_PERIOD';
+export const RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS = 'RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS';
+export const RECEIVE_DELETE_FREEZE_PERIOD_ERROR = 'RECEIVE_DELETE_FREEZE_PERIOD_ERROR';
+
export const RESET_MODAL = 'RESET_MODAL';
diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js
index fdd1ea6e32e..151f7f39f5a 100644
--- a/app/assets/javascripts/deploy_freeze/store/mutations.js
+++ b/app/assets/javascripts/deploy_freeze/store/mutations.js
@@ -1,15 +1,28 @@
+import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { secondsToHours } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
-const formatTimezoneName = (freezePeriod, timezoneList) =>
- convertObjectPropsToCamelCase({
+const formatTimezoneName = (freezePeriod, timezoneList) => {
+ const tz = timezoneList.find((timezone) => timezone.identifier === freezePeriod.cron_timezone);
+ return convertObjectPropsToCamelCase({
...freezePeriod,
cron_timezone: {
- formattedTimezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)
- ?.name,
+ formattedTimezone: tz && `[UTC ${secondsToHours(tz.offset)}] ${tz.name}`,
identifier: freezePeriod.cron_timezone,
},
});
+};
+
+const setFreezePeriodIsDeleting = (state, id, isDeleting) => {
+ const freezePeriod = state.freezePeriods.find((f) => f.id === id);
+
+ if (!freezePeriod) {
+ return;
+ }
+
+ Vue.set(freezePeriod, 'isDeleting', isDeleting);
+};
export default {
[types.REQUEST_FREEZE_PERIODS](state) {
@@ -53,6 +66,18 @@ export default {
state.selectedId = id;
},
+ [types.REQUEST_DELETE_FREEZE_PERIOD](state, id) {
+ setFreezePeriodIsDeleting(state, id, true);
+ },
+
+ [types.RECEIVE_DELETE_FREEZE_PERIOD_SUCCESS](state, id) {
+ state.freezePeriods = state.freezePeriods.filter((f) => f.id !== id);
+ },
+
+ [types.RECEIVE_DELETE_FREEZE_PERIOD_ERROR](state, id) {
+ setFreezePeriodIsDeleting(state, id, false);
+ },
+
[types.RESET_MODAL](state) {
state.freezeStartCron = '';
state.freezeEndCron = '';
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 167bc4c286e..37287b9d981 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -107,7 +107,7 @@ function createLink(data, selected, options, index) {
}
if (options.trackSuggestionClickedLabel) {
- link.setAttribute('data-track-event', 'click_text');
+ link.setAttribute('data-track-action', 'click_text');
link.setAttribute('data-track-label', options.trackSuggestionClickedLabel);
link.setAttribute('data-track-value', index);
link.setAttribute('data-track-property', slugify(data.category || 'no-category'));
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/deprecated_notes.js
index ef51587734d..a42b50edb8a 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -7,7 +7,7 @@ class-methods-use-this */
/* global ResolveService */
/*
-old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
+deprecated_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
*/
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
@@ -670,6 +670,10 @@ export default class Notes {
updateNote(noteEntity, $targetNote) {
// Convert returned HTML to a jQuery object so we can modify it further
const $noteEntityEl = $(noteEntity.html);
+ const $noteAvatar = $noteEntityEl.find('.image-diff-avatar-link');
+ const $targetNoteBadge = $targetNote.find('.badge');
+
+ $noteAvatar.append($targetNoteBadge);
this.revertNoteEditForm($targetNote);
$noteEntityEl.renderGFM();
// Find the note's `li` element by ID and replace it with the updated HTML
@@ -1740,5 +1744,3 @@ export default class Notes {
return $closeBtn.text($closeBtn.data('originalText'));
}
}
-
-window.Notes = Notes;
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 336ce714a05..818299e36bd 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
@@ -123,7 +123,7 @@ export default {
category="primary"
variant="confirm"
type="submit"
- data-track-event="click_button"
+ data-track-action="click_button"
data-qa-selector="save_comment_button"
@click="$emit('submit-form')"
>
diff --git a/app/assets/javascripts/design_management/components/design_scaler.vue b/app/assets/javascripts/design_management/components/design_scaler.vue
index c9273f97bed..af3d4453a6a 100644
--- a/app/assets/javascripts/design_management/components/design_scaler.vue
+++ b/app/assets/javascripts/design_management/components/design_scaler.vue
@@ -1,16 +1,21 @@
<script>
import { GlButtonGroup, GlButton } from '@gitlab/ui';
-const SCALE_STEP_SIZE = 0.2;
const DEFAULT_SCALE = 1;
const MIN_SCALE = 1;
-const MAX_SCALE = 2;
+const ZOOM_LEVELS = 5;
export default {
components: {
GlButtonGroup,
GlButton,
},
+ props: {
+ maxScale: {
+ type: Number,
+ required: true,
+ },
+ },
data() {
return {
scale: DEFAULT_SCALE,
@@ -24,7 +29,10 @@ export default {
return this.scale === DEFAULT_SCALE;
},
disableIncrease() {
- return this.scale >= MAX_SCALE;
+ return this.scale >= this.maxScale;
+ },
+ stepSize() {
+ return (this.maxScale - MIN_SCALE) / ZOOM_LEVELS;
},
},
methods: {
@@ -37,10 +45,10 @@ export default {
this.$emit('scale', this.scale);
},
incrementScale() {
- this.setScale(this.scale + SCALE_STEP_SIZE);
+ this.setScale(Math.min(this.scale + this.stepSize, this.maxScale));
},
decrementScale() {
- this.setScale(this.scale - SCALE_STEP_SIZE);
+ this.setScale(Math.max(this.scale - this.stepSize, MIN_SCALE));
},
resetScale() {
this.setScale(DEFAULT_SCALE);
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index efa1ef2107a..ced76eb4843 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -202,7 +202,7 @@ export default {
data-testid="resolved-discussion"
@error="$emit('onDesignDiscussionError', $event)"
@updateNoteError="$emit('updateNoteError', $event)"
- @openForm="updateDiscussionWithOpenForm"
+ @open-form="updateDiscussionWithOpenForm"
@click.native.stop="updateActiveDiscussion(discussion.notes[0].id)"
/>
</gl-collapse>
diff --git a/app/assets/javascripts/design_management/components/image.vue b/app/assets/javascripts/design_management/components/image.vue
index 8ab94cd2c4b..5354c7756f5 100644
--- a/app/assets/javascripts/design_management/components/image.vue
+++ b/app/assets/javascripts/design_management/components/image.vue
@@ -57,6 +57,7 @@ export default {
methods: {
onImgLoad() {
requestIdleCallback(this.setBaseImageSize, { timeout: 1000 });
+ requestIdleCallback(this.setImageNaturalScale, { timeout: 1000 });
performanceMarkAndMeasure({
measures: [
{
@@ -79,6 +80,27 @@ export default {
};
this.onResize({ width: this.baseImageSize.width, height: this.baseImageSize.height });
},
+ setImageNaturalScale() {
+ const { contentImg } = this.$refs;
+
+ if (!contentImg) {
+ return;
+ }
+
+ const { naturalHeight, naturalWidth } = contentImg;
+
+ // In case image 404s
+ if (naturalHeight === 0 || naturalWidth === 0) {
+ return;
+ }
+
+ const { height, width } = this.baseImageSize;
+
+ this.$parent.$emit(
+ 'setMaxScale',
+ Math.round(((height + width) / (naturalHeight + naturalWidth)) * 100) / 100,
+ );
+ },
onResize({ width, height }) {
this.$emit('resize', { width, height });
},
diff --git a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
index 7eb40b12f51..b715633a9f2 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/version.fragment.graphql
@@ -1,4 +1,11 @@
fragment VersionListItem on DesignVersion {
id
sha
+ createdAt
+ author {
+ __typename
+ id
+ name
+ avatarUrl
+ }
}
diff --git a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
index 84aeb374351..111f5ac18a7 100644
--- a/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
+++ b/app/assets/javascripts/design_management/graphql/mutations/upload_design.mutation.graphql
@@ -1,13 +1,15 @@
#import "../fragments/design.fragment.graphql"
+#import "../fragments/version.fragment.graphql"
mutation uploadDesign($files: [Upload!]!, $projectPath: ID!, $iid: ID!) {
designManagementUpload(input: { projectPath: $projectPath, iid: $iid, files: $files }) {
designs {
...DesignItem
versions {
+ __typename
nodes {
- id
- sha
+ __typename
+ ...VersionListItem
}
}
}
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index 48ee7068809..38ea5406c02 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -46,6 +46,7 @@ import {
import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
const DEFAULT_SCALE = 1;
+const DEFAULT_MAX_SCALE = 2;
export default {
components: {
@@ -96,6 +97,7 @@ export default {
scale: DEFAULT_SCALE,
resolvedDiscussionsExpanded: false,
prevCurrentUserTodos: null,
+ maxScale: DEFAULT_MAX_SCALE,
};
},
apollo: {
@@ -309,9 +311,7 @@ export default {
this.isLatestVersion,
);
- if (this.glFeatures.usageDataDesignAction) {
- servicePingDesignDetailView();
- }
+ servicePingDesignDetailView();
},
updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) {
this.$apollo.mutate({
@@ -330,6 +330,9 @@ export default {
toggleResolvedComments() {
this.resolvedDiscussionsExpanded = !this.resolvedDiscussionsExpanded;
},
+ setMaxScale(event) {
+ this.maxScale = 1 / event;
+ },
},
createImageDiffNoteMutation,
DESIGNS_ROUTE_NAME,
@@ -378,12 +381,13 @@ export default {
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"
@moveNote="onMoveNote"
+ @setMaxScale="setMaxScale"
/>
<div
class="design-scaler-wrapper gl-absolute gl-mb-6 gl-display-flex gl-justify-content-center gl-align-items-center"
>
- <design-scaler @scale="scale = $event" />
+ <design-scaler :max-scale="maxScale" @scale="scale = $event" />
</div>
</div>
<design-sidebar
diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue
index ad557f64ce4..e66ae822a34 100644
--- a/app/assets/javascripts/design_management/pages/index.vue
+++ b/app/assets/javascripts/design_management/pages/index.vue
@@ -140,6 +140,9 @@ export default {
this.$el.scrollIntoView();
}
},
+ beforeDestroy() {
+ document.removeEventListener('paste', this.onDesignPaste);
+ },
methods: {
resetFilesToBeSaved() {
this.filesToBeSaved = [];
diff --git a/app/assets/javascripts/design_management/utils/design_management_utils.js b/app/assets/javascripts/design_management/utils/design_management_utils.js
index 05b220801f2..7470f3d259b 100644
--- a/app/assets/javascripts/design_management/utils/design_management_utils.js
+++ b/app/assets/javascripts/design_management/utils/design_management_utils.js
@@ -85,6 +85,13 @@ export const designUploadOptimisticResponse = (files) => {
__typename: 'DesignVersion',
id: -uniqueId(),
sha: -uniqueId(),
+ createdAt: '',
+ author: {
+ __typename: 'UserCore',
+ id: -uniqueId(),
+ name: '',
+ avatarUrl: '',
+ },
},
},
}));
diff --git a/app/assets/javascripts/design_management/utils/error_messages.js b/app/assets/javascripts/design_management/utils/error_messages.js
index e7b2c814bb3..afee7e81791 100644
--- a/app/assets/javascripts/design_management/utils/error_messages.js
+++ b/app/assets/javascripts/design_management/utils/error_messages.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { __, s__, n__, sprintf } from '~/locale';
export const ADD_DISCUSSION_COMMENT_ERROR = s__(
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index d03b5cbc26b..a2ea42e963c 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -1,9 +1,8 @@
<script>
-import { GlLoadingIcon, GlPagination, GlSprintf } from '@gitlab/ui';
+import { GlLoadingIcon, GlPagination, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Mousetrap from 'mousetrap';
import { mapState, mapGetters, mapActions } from 'vuex';
-import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import api from '~/api';
import {
keysFor,
@@ -47,7 +46,6 @@ import {
import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
-import { fileByFile } from '../utils/preferences';
import { queueRedisHllEvents } from '../utils/queue_events';
import CollapsedFilesWarning from './collapsed_files_warning.vue';
import CommitWidget from './commit_widget.vue';
@@ -55,13 +53,18 @@ import CompareVersions from './compare_versions.vue';
import DiffFile from './diff_file.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import NoChanges from './no_changes.vue';
-import PreRenderer from './pre_renderer.vue';
import TreeList from './tree_list.vue';
-import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
export default {
name: 'DiffsApp',
components: {
+ DynamicScroller: () =>
+ import('vendor/vue-virtual-scroller').then(({ DynamicScroller }) => DynamicScroller),
+ DynamicScrollerItem: () =>
+ import('vendor/vue-virtual-scroller').then(({ DynamicScrollerItem }) => DynamicScrollerItem),
+ PreRenderer: () => import('./pre_renderer.vue').then((PreRenderer) => PreRenderer),
+ VirtualScrollerScrollSync: () =>
+ import('./virtual_scroller_scroll_sync').then((VSSSync) => VSSSync),
CompareVersions,
DiffFile,
NoChanges,
@@ -73,11 +76,8 @@ export default {
PanelResizer,
GlPagination,
GlSprintf,
- DynamicScroller,
- DynamicScrollerItem,
- PreRenderer,
- VirtualScrollerScrollSync,
MrWidgetHowToMergeModal,
+ GlAlert,
},
alerts: {
ALERT_OVERFLOW_HIDDEN,
@@ -189,25 +189,24 @@ export default {
treeWidth,
diffFilesLength: 0,
virtualScrollCurrentIndex: -1,
+ subscribedToVirtualScrollingEvents: false,
};
},
computed: {
- ...mapState({
- isLoading: (state) => state.diffs.isLoading,
- isBatchLoading: (state) => state.diffs.isBatchLoading,
- diffFiles: (state) => state.diffs.diffFiles,
- diffViewType: (state) => state.diffs.diffViewType,
- commit: (state) => state.diffs.commit,
- renderOverflowWarning: (state) => state.diffs.renderOverflowWarning,
- numTotalFiles: (state) => state.diffs.realSize,
- numVisibleFiles: (state) => state.diffs.size,
- plainDiffPath: (state) => state.diffs.plainDiffPath,
- emailPatchPath: (state) => state.diffs.emailPatchPath,
- retrievingBatches: (state) => state.diffs.retrievingBatches,
+ ...mapState('diffs', {
+ numTotalFiles: 'realSize',
+ numVisibleFiles: 'size',
}),
...mapState('diffs', [
'showTreeList',
'isLoading',
+ 'diffFiles',
+ 'diffViewType',
+ 'commit',
+ 'renderOverflowWarning',
+ 'plainDiffPath',
+ 'emailPatchPath',
+ 'retrievingBatches',
'startVersion',
'latestDiff',
'currentDiffFileId',
@@ -227,8 +226,9 @@ export default {
'isParallelView',
'currentDiffIndex',
'isVirtualScrollingEnabled',
+ 'isBatchLoading',
+ 'isBatchLoadingError',
]),
- ...mapGetters('batchComments', ['draftsCount']),
...mapGetters(['isNotesFetched', 'getNoteableData']),
diffs() {
if (!this.viewDiffsFileByFile) {
@@ -316,6 +316,7 @@ export default {
}
this.adjustView();
+ this.subscribeToVirtualScrollingEvents();
},
isLoading: 'adjustView',
renderFileTree: 'adjustView',
@@ -330,7 +331,7 @@ export default {
projectPath: this.projectPath,
dismissEndpoint: this.dismissEndpoint,
showSuggestPopover: this.showSuggestPopover,
- viewDiffsFileByFile: fileByFile(this.fileByFileUserPreference),
+ viewDiffsFileByFile: this.fileByFileUserPreference || false,
defaultSuggestionCommitMessage: this.defaultSuggestionCommitMessage,
mrReviews: this.rehydratedMrReviews,
});
@@ -349,11 +350,6 @@ export default {
this.setHighlightedRow(id.split('diff-content').pop().slice(1));
}
- if (window.gon?.features?.diffsVirtualScrolling) {
- diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
- diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex);
- }
-
if (window.gon?.features?.diffSettingsUsageData) {
const events = [];
@@ -383,6 +379,8 @@ export default {
queueRedisHllEvents(events);
}
+
+ this.subscribeToVirtualScrollingEvents();
},
beforeCreate() {
diffsApp.instrument();
@@ -611,6 +609,21 @@ export default {
}
}
},
+ subscribeToVirtualScrollingEvents() {
+ if (
+ window.gon?.features?.diffsVirtualScrolling &&
+ this.shouldShow &&
+ !this.subscribedToVirtualScrollingEvents
+ ) {
+ diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
+ diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex);
+
+ this.subscribedToVirtualScrollingEvents = true;
+ }
+ },
+ reloadPage() {
+ window.location.reload();
+ },
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
@@ -629,17 +642,19 @@ export default {
:diff-files-count-text="numTotalFiles"
/>
- <hidden-files-warning
- v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN"
- :visible="numVisibleFiles"
- :total="numTotalFiles"
- :plain-diff-path="plainDiffPath"
- :email-patch-path="emailPatchPath"
- />
- <collapsed-files-warning
- v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
- :limited="isLimitedContainer"
- />
+ <template v-if="!isBatchLoadingError">
+ <hidden-files-warning
+ v-if="visibleWarning == $options.alerts.ALERT_OVERFLOW_HIDDEN"
+ :visible="numVisibleFiles"
+ :total="numTotalFiles"
+ :plain-diff-path="plainDiffPath"
+ :email-patch-path="emailPatchPath"
+ />
+ <collapsed-files-warning
+ v-if="visibleWarning == $options.alerts.ALERT_COLLAPSED_FILES"
+ :limited="isLimitedContainer"
+ />
+ </template>
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"
@@ -648,7 +663,6 @@ export default {
<div
v-if="renderFileTree"
:style="{ width: `${treeWidth}px` }"
- :class="{ 'review-bar-visible': draftsCount > 0 }"
class="diff-tree-list js-diff-tree-list px-3 pr-md-0"
>
<panel-resizer
@@ -668,11 +682,21 @@ export default {
}"
>
<commit-widget v-if="commit" :commit="commit" :collapsible="false" />
- <div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div>
+ <gl-alert
+ v-if="isBatchLoadingError"
+ variant="danger"
+ :dismissible="false"
+ :primary-button-text="__('Reload page')"
+ @primaryAction="reloadPage"
+ >
+ {{ __("Error: Couldn't load some or all of the changes.") }}
+ </gl-alert>
+ <div v-if="isBatchLoading && !isBatchLoadingError" class="loading">
+ <gl-loading-icon size="lg" />
+ </div>
<template v-else-if="renderDiffFiles">
<dynamic-scroller
v-if="isVirtualScrollingEnabled"
- ref="virtualScroller"
:items="diffs"
:min-item-size="70"
:buffer="1000"
@@ -745,7 +769,10 @@ export default {
</div>
<gl-loading-icon v-else-if="retrievingBatches" size="lg" />
</template>
- <no-changes v-else :changes-empty-state-illustration="changesEmptyStateIllustration" />
+ <no-changes
+ v-else-if="!isBatchLoadingError"
+ :changes-empty-state-illustration="changesEmptyStateIllustration"
+ />
</div>
</div>
<mr-widget-how-to-merge-modal
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 820c64a9502..4435a533591 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
@@ -100,7 +99,10 @@ export default {
<div
class="commit-actions flex-row d-none d-sm-flex align-items-start flex-wrap justify-content-end"
>
- <div v-if="commit.signature_html" v-html="commit.signature_html"></div>
+ <div
+ v-if="commit.signature_html"
+ v-html="commit.signature_html /* eslint-disable-line vue/no-v-html */"
+ ></div>
<commit-pipeline-status
v-if="commit.pipeline_status_path"
:endpoint="commit.pipeline_status_path"
@@ -142,7 +144,7 @@ export default {
<a
:href="commit.commit_url"
class="commit-row-message item-title"
- v-html="commit.title_html"
+ v-html="commit.title_html /* eslint-disable-line vue/no-v-html */"
></a>
<span class="commit-row-message d-block d-sm-none">&middot; {{ commit.short_id }}</span>
@@ -174,7 +176,7 @@ export default {
v-if="commit.description_html"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 gl-text-body"
- v-html="commitDescription"
+ v-html="commitDescription /* eslint-disable-line vue/no-v-html */"
></pre>
</div>
</li>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 933891d698c..d09cc064b2c 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -170,10 +170,7 @@ export default {
return !this.isCollapsed && !this.isFileTooLarge;
},
showLocalFileReviews() {
- const loggedIn = Boolean(gon.current_user_id);
- const featureOn = this.glFeatures.localFileReviews;
-
- return loggedIn && featureOn;
+ return Boolean(gon.current_user_id);
},
codequalityDiffForFile() {
return this.codequalityDiff?.files?.[this.file.file_path] || [];
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 667b8745f7b..4bcb99424db 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -341,7 +341,7 @@ export default {
:gfm="gfmCopyText"
data-testid="diff-file-copy-clipboard"
category="tertiary"
- data-track-event="click_copy_file_button"
+ data-track-action="click_copy_file_button"
data-track-label="diff_copy_file_path_button"
data-track-property="diff_copy_file"
/>
@@ -382,7 +382,7 @@ export default {
:title="externalUrlLabel"
:aria-label="externalUrlLabel"
target="_blank"
- data-track-event="click_toggle_external_button"
+ data-track-action="click_toggle_external_button"
data-track-label="diff_toggle_external_button"
data-track-property="diff_toggle_external"
icon="external-link"
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index db3ad074d2f..737c4d8f33c 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { memoize } from 'lodash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import {
@@ -268,7 +267,7 @@ export default {
]"
class="diff-td line_content with-coverage left-side"
data-testid="left-content"
- v-html="$options.lineContent(props.line.left)"
+ v-html="$options.lineContent(props.line.left) /* eslint-disable-line vue/no-v-html */"
></div>
</template>
<template
@@ -390,7 +389,7 @@ export default {
},
]"
class="diff-td line_content with-coverage right-side parallel"
- v-html="$options.lineContent(props.line.right)"
+ v-html="$options.lineContent(props.line.right) /* eslint-disable-line vue/no-v-html */"
></div>
</template>
<template v-else>
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 5cf242b4ddd..64ded1ca8ca 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -133,7 +133,10 @@ export default {
<template>
<div
- :class="[$options.userColorScheme, { inline, 'with-codequality': hasCodequalityChanges }]"
+ :class="[
+ $options.userColorScheme,
+ { 'inline-diff-view': inline, 'with-codequality': hasCodequalityChanges },
+ ]"
:data-commit-id="commitId"
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
@mousedown="handleParallelLineMouseDown"
diff --git a/app/assets/javascripts/diffs/components/pre_renderer.vue b/app/assets/javascripts/diffs/components/pre_renderer.vue
index c357aa2d924..e4320c40d2c 100644
--- a/app/assets/javascripts/diffs/components/pre_renderer.vue
+++ b/app/assets/javascripts/diffs/components/pre_renderer.vue
@@ -17,7 +17,6 @@ export default {
},
mounted() {
this.width = this.$el.parentNode.offsetWidth;
- window.test = this;
this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
await this.$nextTick();
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 8dda5eadb16..93961b07e2e 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -72,11 +72,6 @@ export const ALERT_COLLAPSED_FILES = 'collapsed';
export const DIFF_FILE_AUTOMATIC_COLLAPSE = 'automatic';
export const DIFF_FILE_MANUAL_COLLAPSE = 'manual';
-// Diff view single file mode
-export const DIFF_FILE_BY_FILE_COOKIE_NAME = 'fileViewMode';
-export const DIFF_VIEW_FILE_BY_FILE = 'single';
-export const DIFF_VIEW_ALL_FILES = 'all';
-
// State machine states
export const STATE_IDLING = 'idle';
export const STATE_LOADING = 'loading';
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index bddc28c4758..1b1ab59b2b4 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -3,7 +3,6 @@ import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import { getParameterValues } from '~/lib/utils/url_utility';
-import FindFile from '~/vue_shared/components/file_finder/index.vue';
import eventHub from '../notes/event_hub';
import diffsApp from './components/app.vue';
@@ -12,51 +11,7 @@ import { getReviewsForMergeRequest } from './utils/file_reviews';
import { getDerivedMergeRequestInformation } from './utils/merge_request';
export default function initDiffsApp(store) {
- const fileFinderEl = document.getElementById('js-diff-file-finder');
-
- if (fileFinderEl) {
- // eslint-disable-next-line no-new
- new Vue({
- el: fileFinderEl,
- store,
- computed: {
- ...mapState('diffs', ['fileFinderVisible', 'isLoading']),
- ...mapGetters('diffs', ['flatBlobsList']),
- },
- watch: {
- fileFinderVisible(newVal, oldVal) {
- if (newVal && !oldVal && !this.flatBlobsList.length) {
- eventHub.$emit('fetchDiffData');
- }
- },
- },
- methods: {
- ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
- openFile(file) {
- window.mrTabs.tabShown('diffs');
- this.scrollToFile(file.path);
- },
- },
- render(createElement) {
- return createElement(FindFile, {
- props: {
- files: this.flatBlobsList,
- visible: this.fileFinderVisible,
- loading: this.isLoading,
- showDiffStats: true,
- clearSearchOnClose: false,
- },
- on: {
- toggle: this.toggleFileFinder,
- click: this.openFile,
- },
- class: ['diff-file-finder'],
- });
- },
- });
- }
-
- return new Vue({
+ const vm = new Vue({
el: '#js-diffs-app',
name: 'MergeRequestDiffs',
components: {
@@ -157,4 +112,53 @@ export default function initDiffsApp(store) {
});
},
});
+
+ const fileFinderEl = document.getElementById('js-diff-file-finder');
+
+ if (fileFinderEl) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: fileFinderEl,
+ store,
+ components: {
+ FindFile: () => import('~/vue_shared/components/file_finder/index.vue'),
+ },
+ computed: {
+ ...mapState('diffs', ['fileFinderVisible', 'isLoading']),
+ ...mapGetters('diffs', ['flatBlobsList']),
+ },
+ watch: {
+ fileFinderVisible(newVal, oldVal) {
+ if (newVal && !oldVal && !this.flatBlobsList.length) {
+ eventHub.$emit('fetchDiffData');
+ }
+ },
+ },
+ methods: {
+ ...mapActions('diffs', ['toggleFileFinder', 'scrollToFile']),
+ openFile(file) {
+ window.mrTabs.tabShown('diffs');
+ this.scrollToFile(file.path);
+ },
+ },
+ render(createElement) {
+ return createElement('find-file', {
+ props: {
+ files: this.flatBlobsList,
+ visible: this.fileFinderVisible,
+ loading: this.isLoading,
+ showDiffStats: true,
+ clearSearchOnClose: false,
+ },
+ on: {
+ toggle: this.toggleFileFinder,
+ click: this.openFile,
+ },
+ class: ['diff-file-finder'],
+ });
+ },
+ });
+ }
+
+ return vm;
}
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index f7bdbe94bac..5c94c6b803b 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -29,9 +29,6 @@ import {
EVT_PERF_MARK_FILE_TREE_START,
EVT_PERF_MARK_FILE_TREE_END,
EVT_PERF_MARK_DIFF_FILES_START,
- DIFF_VIEW_FILE_BY_FILE,
- DIFF_VIEW_ALL_FILES,
- DIFF_FILE_BY_FILE_COOKIE_NAME,
TRACKING_CLICK_DIFF_VIEW_SETTING,
TRACKING_DIFF_VIEW_INLINE,
TRACKING_DIFF_VIEW_PARALLEL,
@@ -104,7 +101,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
let totalLoaded = 0;
let scrolledVirtualScroller = false;
- commit(types.SET_BATCH_LOADING, true);
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
commit(types.SET_RETRIEVING_BATCHES, true);
eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START);
@@ -115,7 +112,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
totalLoaded += diff_files.length;
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
- commit(types.SET_BATCH_LOADING, false);
+ commit(types.SET_BATCH_LOADING_STATE, 'loaded');
if (window.gon?.features?.diffsVirtualScrolling && !scrolledVirtualScroller) {
const index = state.diffFiles.findIndex(
@@ -130,7 +127,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
}
if (!isNoteLink && !state.currentDiffFileId) {
- commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
+ commit(types.VIEW_DIFF_FILE, diff_files[0]?.file_hash);
}
if (isNoteLink) {
@@ -182,11 +179,14 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
return null;
})
- .catch(() => commit(types.SET_RETRIEVING_BATCHES, false));
+ .catch(() => {
+ commit(types.SET_RETRIEVING_BATCHES, false);
+ commit(types.SET_BATCH_LOADING_STATE, 'error');
+ });
- return getBatch()
- .then(() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash())
- .catch(() => null);
+ return getBatch().then(
+ () => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash(),
+ );
};
export const fetchDiffFilesMeta = ({ commit, state }) => {
@@ -816,9 +816,7 @@ export const navigateToDiffFileIndex = ({ commit, state }, index) => {
};
export const setFileByFile = ({ state, commit }, { fileByFile }) => {
- const fileViewMode = fileByFile ? DIFF_VIEW_FILE_BY_FILE : DIFF_VIEW_ALL_FILES;
commit(types.SET_FILE_BY_FILE, fileByFile);
- Cookies.set(DIFF_FILE_BY_FILE_COOKIE_NAME, fileViewMode);
if (window.gon?.features?.diffSettingsUsageData) {
const events = [TRACKING_CLICK_SINGLE_FILE_SETTING];
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 18bd8e5f1d8..ca85be5d829 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -191,3 +191,6 @@ export const isVirtualScrollingEnabled = (state) => {
getParameterValues('virtual_scrolling')[0] === 'true')
);
};
+
+export const isBatchLoading = (state) => state.batchLoadingState === 'loading';
+export const isBatchLoadingError = (state) => state.batchLoadingState === 'error';
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index d76361513d4..a5b1a577a78 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -2,8 +2,6 @@ import Cookies from 'js-cookie';
import { getParameterValues } from '~/lib/utils/url_utility';
import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
-import { fileByFile } from '../../utils/preferences';
-
const getViewTypeFromQueryString = () => getParameterValues('view')[0];
const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
@@ -12,7 +10,7 @@ const defaultViewType = INLINE_DIFF_VIEW_TYPE;
export default () => ({
isLoading: true,
isTreeLoaded: false,
- isBatchLoading: false,
+ batchLoadingState: null,
retrievingBatches: false,
addedLines: null,
removedLines: null,
@@ -36,7 +34,7 @@ export default () => ({
highlightedRow: null,
renderTreeList: true,
showWhitespace: true,
- viewDiffsFileByFile: fileByFile(),
+ viewDiffsFileByFile: false,
fileFinderVisible: false,
dismissEndpoint: '',
showSuggestPopover: true,
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 2c370221f40..60836f747f5 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -1,6 +1,6 @@
export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
export const SET_LOADING = 'SET_LOADING';
-export const SET_BATCH_LOADING = 'SET_BATCH_LOADING';
+export const SET_BATCH_LOADING_STATE = 'SET_BATCH_LOADING_STATE';
export const SET_RETRIEVING_BATCHES = 'SET_RETRIEVING_BATCHES';
export const SET_DIFF_METADATA = 'SET_DIFF_METADATA';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 1aa83453bf7..6bc927b9d1f 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -60,8 +60,8 @@ export default {
Object.assign(state, { isLoading });
},
- [types.SET_BATCH_LOADING](state, isBatchLoading) {
- Object.assign(state, { isBatchLoading });
+ [types.SET_BATCH_LOADING_STATE](state, batchLoadingState) {
+ Object.assign(state, { batchLoadingState });
},
[types.SET_RETRIEVING_BATCHES](state, retrievingBatches) {
diff --git a/app/assets/javascripts/diffs/utils/preferences.js b/app/assets/javascripts/diffs/utils/preferences.js
deleted file mode 100644
index 6b4aaf45937..00000000000
--- a/app/assets/javascripts/diffs/utils/preferences.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Cookies from 'js-cookie';
-import { DIFF_FILE_BY_FILE_COOKIE_NAME, DIFF_VIEW_FILE_BY_FILE } from '../constants';
-
-export function fileByFile(pref = false) {
- const cookie = Cookies.get(DIFF_FILE_BY_FILE_COOKIE_NAME);
-
- // use the cookie first, if it exists
- if (cookie) {
- return cookie === DIFF_VIEW_FILE_BY_FILE;
- }
-
- return pref;
-}
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 337f7ae2757..f98f63529fc 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -4,6 +4,7 @@ import { escape } from 'lodash';
import './behaviors/preview_markdown';
import { spriteIcon } from '~/lib/utils/common_utils';
import { getFilename } from '~/lib/utils/file_upload';
+import { truncate } from '~/lib/utils/text_utility';
import { n__, __ } from '~/locale';
import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
import axios from './lib/utils/axios_utils';
@@ -189,10 +190,13 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
if (image) {
event.preventDefault();
+ const MAX_FILE_NAME_LENGTH = 246;
const filename = getFilename(pasteEvent) || 'image.png';
- const text = `{{${filename}}}`;
+ const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
+ const text = `{{${truncateFilename}}}`;
pasteText(text);
- return uploadFile(image.getAsFile(), filename);
+
+ return uploadFile(image.getAsFile(), truncateFilename);
}
}
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
deleted file mode 100644
index aa223270f2c..00000000000
--- a/app/assets/javascripts/due_date_select.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/* eslint-disable max-classes-per-file */
-import dateFormat from 'dateformat';
-import $ from 'jquery';
-import Pikaday from 'pikaday';
-import initDatePicker from '~/behaviors/date_picker';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { __ } from '~/locale';
-import boardsStore from './boards/stores/boards_store';
-import axios from './lib/utils/axios_utils';
-import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
-
-class DueDateSelect {
- constructor({ $dropdown, $loading } = {}) {
- const $dropdownParent = $dropdown.closest('.dropdown');
- const $block = $dropdown.closest('.block');
- this.$loading = $loading;
- this.$dropdown = $dropdown;
- this.$dropdownParent = $dropdownParent;
- this.$datePicker = $dropdownParent.find('.js-due-date-calendar');
- this.$block = $block;
- this.$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
- this.$selectbox = $dropdown.closest('.selectbox');
- this.$value = $block.find('.value');
- this.$valueContent = $block.find('.value-content');
- this.$sidebarValue = $('.js-due-date-sidebar-value', $block);
- this.fieldName = $dropdown.data('fieldName');
- this.abilityName = $dropdown.data('abilityName');
- this.issueUpdateURL = $dropdown.data('issueUpdate');
-
- this.rawSelectedDate = null;
- this.displayedDate = null;
- this.datePayload = null;
-
- this.initGlDropdown();
- this.initRemoveDueDate();
- this.initDatePicker();
- }
-
- initGlDropdown() {
- initDeprecatedJQueryDropdown(this.$dropdown, {
- opened: () => {
- const calendar = this.$datePicker.data('pikaday');
- calendar.show();
- },
- hidden: () => {
- this.$selectbox.hide();
- this.$value.css('display', '');
- },
- shouldPropagate: false,
- });
- }
-
- initDatePicker() {
- const $dueDateInput = $(`input[name='${this.fieldName}']`);
- const calendar = new Pikaday({
- field: $dueDateInput.get(0),
- theme: 'gitlab-theme',
- format: 'yyyy-mm-dd',
- parse: (dateString) => parsePikadayDate(dateString),
- toString: (date) => pikadayToString(date),
- onSelect: (dateText) => {
- $dueDateInput.val(calendar.toString(dateText));
-
- if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- boardsStore.detail.issue.dueDate = $dueDateInput.val();
- this.updateIssueBoardIssue();
- } else {
- this.saveDueDate(true);
- }
- },
- firstDay: gon.first_day_of_week,
- });
-
- calendar.setDate(parsePikadayDate($dueDateInput.val()));
- this.$datePicker.append(calendar.el);
- this.$datePicker.data('pikaday', calendar);
- }
-
- initRemoveDueDate() {
- this.$block.on('click', '.js-remove-due-date', (e) => {
- const calendar = this.$datePicker.data('pikaday');
- e.preventDefault();
-
- calendar.setDate(null);
-
- if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
- boardsStore.detail.issue.dueDate = '';
- this.updateIssueBoardIssue();
- } else {
- $(`input[name='${this.fieldName}']`).val('');
- this.saveDueDate(false);
- }
- });
- }
-
- saveDueDate(isDropdown) {
- this.parseSelectedDate();
- this.prepSelectedDate();
- this.submitSelectedDate(isDropdown);
- }
-
- parseSelectedDate() {
- this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
-
- if (this.rawSelectedDate.length) {
- // Construct Date object manually to avoid buggy dateString support within Date constructor
- const dateArray = this.rawSelectedDate.split('-').map((v) => parseInt(v, 10));
- const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
- this.displayedDate = dateFormat(dateObj, 'mmm d, yyyy');
- } else {
- this.displayedDate = __('None');
- }
- }
-
- prepSelectedDate() {
- const datePayload = {};
- datePayload[this.abilityName] = {};
- datePayload[this.abilityName].due_date = this.rawSelectedDate;
- this.datePayload = datePayload;
- }
-
- updateIssueBoardIssue() {
- this.$loading.removeClass('gl-display-none');
- this.$dropdown.trigger('loading.gl.dropdown');
- this.$selectbox.hide();
- this.$value.css('display', '');
- const hideLoader = () => {
- this.$loading.addClass('gl-display-none');
- };
-
- boardsStore.detail.issue
- .update(this.$dropdown.attr('data-issue-update'))
- .then(hideLoader)
- .catch(hideLoader);
- }
-
- submitSelectedDate(isDropdown) {
- const selectedDateValue = this.datePayload[this.abilityName].due_date;
- const hasDueDate = this.displayedDate !== __('None');
- const displayedDateStyle = hasDueDate ? 'bold' : 'no-value';
-
- this.$loading.removeClass('gl-display-none');
-
- if (isDropdown) {
- this.$dropdown.trigger('loading.gl.dropdown');
- this.$selectbox.hide();
- }
-
- this.$value.css('display', '');
- this.$valueContent.html(`<span class='${displayedDateStyle}'>${this.displayedDate}</span>`);
- this.$sidebarValue.html(this.displayedDate);
-
- $('.js-remove-due-date-holder').toggleClass('hidden', selectedDateValue.length);
-
- return axios.put(this.issueUpdateURL, this.datePayload).then(() => {
- const tooltipText = hasDueDate
- ? `${__('Due date')}<br />${selectedDateValue} (${timeFor(selectedDateValue)})`
- : __('Due date');
- if (isDropdown) {
- this.$dropdown.trigger('loaded.gl.dropdown');
- this.$dropdown.dropdown('toggle');
- }
- this.$sidebarCollapsedValue.attr('data-original-title', tooltipText);
-
- return this.$loading.addClass('gl-display-none');
- });
- }
-}
-
-export default class DueDateSelectors {
- constructor() {
- initDatePicker();
- this.initIssuableSelect();
- }
- // eslint-disable-next-line class-methods-use-this
- initIssuableSelect() {
- const $loading = $('.js-issuable-update .due_date')
- .find('.block-loading')
- .removeClass('hidden')
- .addClass('gl-display-none');
-
- $('.js-due-date-select').each((i, dropdown) => {
- const $dropdown = $(dropdown);
- // eslint-disable-next-line no-new
- new DueDateSelect({
- $dropdown,
- $loading,
- });
- });
- }
-}
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index 7faf0fe5f08..7672151af2a 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -1,5 +1,6 @@
import { escape, minBy } from 'lodash';
import emojiAliases from 'emojis/aliases.json';
+import { sanitize } from '~/lib/dompurify';
import AccessorUtilities from '../lib/utils/accessor';
import axios from '../lib/utils/axios_utils';
import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
@@ -10,7 +11,7 @@ export const FALLBACK_EMOJI_KEY = 'grey_question';
export const EMOJI_VERSION = '1';
-const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
async function loadEmoji() {
if (
@@ -34,7 +35,7 @@ async function loadEmoji() {
async function loadEmojiWithNames() {
return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
- acc[key] = { ...value, name: key };
+ acc[key] = { ...value, name: key, e: sanitize(value.e) };
return acc;
}, {});
diff --git a/app/assets/javascripts/emoji/support/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js
index fe3bc75f9fd..d90a774c293 100644
--- a/app/assets/javascripts/emoji/support/unicode_support_map.js
+++ b/app/assets/javascripts/emoji/support/unicode_support_map.js
@@ -141,7 +141,7 @@ function generateUnicodeSupportMap(testMap) {
}
export default function getUnicodeSupportMap() {
- const isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
let glEmojiVersionFromCache;
let userAgentFromCache;
diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue
index 9e058af56c4..cec53869aa8 100644
--- a/app/assets/javascripts/environments/components/container.vue
+++ b/app/assets/javascripts/environments/components/container.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
},
methods: {
onChangePage(page) {
@@ -42,7 +38,7 @@ export default {
<slot name="empty-state"></slot>
<div v-if="!isLoading && environments.length > 0" class="table-holder">
- <environment-table :environments="environments" :can-read-environment="canReadEnvironment" />
+ <environment-table :environments="environments" />
<table-pagination
v-if="pagination && pagination.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue
index 1cd960d7cd6..96742a11ebb 100644
--- a/app/assets/javascripts/environments/components/edit_environment.vue
+++ b/app/assets/javascripts/environments/components/edit_environment.vue
@@ -18,6 +18,7 @@ export default {
data() {
return {
formEnvironment: {
+ id: this.environment.id,
name: this.environment.name,
externalUrl: this.environment.external_url,
},
@@ -33,7 +34,6 @@ export default {
axios
.put(this.updateEnvironmentPath, {
id: this.environment.id,
- name: this.formEnvironment.name,
external_url: this.formEnvironment.externalUrl,
})
.then(({ data: { path } }) => visitUrl(path))
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index 6db8fe24e72..1d1d8d61b66 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -39,12 +39,17 @@ export default {
),
nameLabel: __('Name'),
nameFeedback: __('This field is required'),
+ nameDisabledHelp: __("You cannot rename an environment after it's created."),
+ nameDisabledLinkText: __('How do I rename an environment?'),
urlLabel: __('External URL'),
urlFeedback: __('The URL should start with http:// or https://'),
save: __('Save'),
cancel: __('Cancel'),
},
helpPagePath: helpPagePath('ci/environments/index.md'),
+ renamingDisabledHelpPagePath: helpPagePath('ci/environments/index.md', {
+ anchor: 'rename-an-environment',
+ }),
data() {
return {
visited: {
@@ -54,6 +59,9 @@ export default {
};
},
computed: {
+ isNameDisabled() {
+ return Boolean(this.environment.id);
+ },
valid() {
return {
name: this.visited.name && this.environment.name !== '',
@@ -102,10 +110,17 @@ export default {
:state="valid.name"
:invalid-feedback="$options.i18n.nameFeedback"
>
+ <template v-if="isNameDisabled" #description>
+ {{ $options.i18n.nameDisabledHelp }}
+ <gl-link :href="$options.renamingDisabledHelpPagePath" target="_blank">
+ {{ $options.i18n.nameDisabledLinkText }}
+ </gl-link>
+ </template>
<gl-form-input
id="environment_name"
:value="environment.name"
:state="valid.name"
+ :disabled="isNameDisabled"
name="environment[name]"
required
@input="onChange({ ...environment, name: $event })"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 897f6ce393e..d12863ee742 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__, sprintf } from '~/locale';
@@ -32,6 +31,7 @@ export default {
ExternalUrlComponent,
GlIcon,
GlLink,
+ GlSprintf,
MonitoringButtonComponent,
PinComponent,
DeleteComponent,
@@ -48,12 +48,6 @@ export default {
mixins: [timeagoMixin],
props: {
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
-
model: {
type: Object,
required: true,
@@ -647,14 +641,17 @@ export default {
</span>
<span v-if="!isFolder && deploymentHasUser" class="text-break-word">
- by
- <user-avatar-link
- :link-href="deploymentUser.web_url"
- :img-src="deploymentUser.avatar_url"
- :img-alt="userImageAltDescription"
- :tooltip-text="deploymentUser.username"
- class="js-deploy-user-container float-none"
- />
+ <gl-sprintf :message="s__('Environments|by %{avatar}')">
+ <template #avatar>
+ <user-avatar-link
+ :link-href="deploymentUser.web_url"
+ :img-src="deploymentUser.avatar_url"
+ :img-alt="userImageAltDescription"
+ :tooltip-text="deploymentUser.username"
+ class="js-deploy-user-container float-none"
+ />
+ </template>
+ </gl-sprintf>
</span>
<div v-if="showNoDeployments" class="commit-title table-mobile-content">
@@ -743,13 +740,16 @@ export default {
</div>
<div class="gl-display-flex">
<span v-if="upcomingDeployment.user" class="text-break-word">
- by
- <user-avatar-link
- :link-href="upcomingDeployment.user.web_url"
- :img-src="upcomingDeployment.user.avatar_url"
- :img-alt="upcomingDeploymentUserImageAltDescription"
- :tooltip-text="upcomingDeployment.user.username"
- />
+ <gl-sprintf :message="s__('Environments|by %{avatar}')">
+ <template #avatar>
+ <user-avatar-link
+ :link-href="upcomingDeployment.user.web_url"
+ :img-src="upcomingDeployment.user.avatar_url"
+ :img-alt="upcomingDeploymentUserImageAltDescription"
+ :tooltip-text="upcomingDeployment.user.username"
+ />
+ </template>
+ </gl-sprintf>
</span>
</div>
</div>
@@ -784,14 +784,14 @@ export default {
/>
<external-url-component
- v-if="externalURL && canReadEnvironment"
+ v-if="externalURL"
:external-url="externalURL"
data-track-action="click_button"
data-track-label="environment_url"
/>
<monitoring-button-component
- v-if="monitoringUrl && canReadEnvironment"
+ v-if="monitoringUrl"
:monitoring-url="monitoringUrl"
data-track-action="click_button"
data-track-label="environment_monitoring"
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 105315dcf51..acc16ecd874 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,9 +1,7 @@
<script>
-import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs, GlAlert } from '@gitlab/ui';
+import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
import createFlash from '~/flash';
-import { setCookie, getCookie, parseBoolean } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import { ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME } from '../constants';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
@@ -17,12 +15,6 @@ export default {
i18n: {
newEnvironmentButtonLabel: s__('Environments|New environment'),
reviewAppButtonLabel: s__('Environments|Enable review app'),
- surveyAlertTitle: s__('Environments|Help us improve environments'),
- surveyAlertText: s__(
- 'Environments|Your feedback helps GitLab make environments better for you and other users. Participate and enter a sweepstake to win a USD 30 gift card.',
- ),
- surveyAlertButtonLabel: s__('Environments|Take the survey'),
- surveyDismissButtonLabel: s__('Environments|Dismiss'),
},
modal: {
id: 'enable-review-app-info',
@@ -33,7 +25,6 @@ export default {
EnableReviewAppModal,
GlBadge,
GlButton,
- GlAlert,
GlTab,
GlTabs,
StopEnvironmentModal,
@@ -52,10 +43,6 @@ export default {
type: Boolean,
required: true,
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
newEnvironmentPath: {
type: String,
required: true,
@@ -65,13 +52,6 @@ export default {
required: true,
},
},
- data() {
- return {
- environmentsSurveyAlertDismissed: parseBoolean(
- getCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME),
- ),
- };
- },
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
@@ -121,11 +101,6 @@ export default {
openFolders.forEach((folder) => this.fetchChildEnvironments(folder));
}
},
-
- onSurveyAlertDismiss() {
- setCookie(ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME, 'true');
- this.environmentsSurveyAlertDismissed = true;
- },
},
};
</script>
@@ -156,19 +131,6 @@ export default {
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
>
</div>
- <gl-alert
- v-if="!environmentsSurveyAlertDismissed"
- class="gl-my-4"
- :title="$options.i18n.surveyAlertTitle"
- :primary-button-text="$options.i18n.surveyAlertButtonLabel"
- variant="info"
- dismissible
- :dismiss-label="$options.i18n.surveyDismissButtonLabel"
- primary-button-link="https://gitlab.fra1.qualtrics.com/jfe/form/SV_a2xyFsAA4D0w0Jg"
- @dismiss="onSurveyAlertDismiss"
- >
- {{ $options.i18n.surveyAlertText }}
- </gl-alert>
<gl-tabs :value="activeTab" content-class="gl-display-none">
<gl-tab
v-for="(tab, idx) in tabs"
@@ -210,7 +172,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
>
<template v-if="!isLoading && state.environments.length === 0" #empty-state>
diff --git a/app/assets/javascripts/environments/components/environments_detail_header.vue b/app/assets/javascripts/environments/components/environments_detail_header.vue
index 467c89fd8b8..d71b553a878 100644
--- a/app/assets/javascripts/environments/components/environments_detail_header.vue
+++ b/app/assets/javascripts/environments/components/environments_detail_header.vue
@@ -27,10 +27,6 @@ export default {
type: Object,
required: true,
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
canAdminEnvironment: {
type: Boolean,
required: true,
@@ -84,7 +80,7 @@ export default {
return this.environment.isAvailable && Boolean(this.environment.autoStopAt);
},
shouldShowExternalUrlButton() {
- return this.canReadEnvironment && Boolean(this.environment.externalUrl);
+ return Boolean(this.environment.externalUrl);
},
shouldShowStopButton() {
return this.canStopEnvironment && this.environment.isAvailable;
@@ -138,7 +134,7 @@ export default {
>{{ $options.i18n.externalButtonText }}</gl-button
>
<gl-button
- v-if="canReadEnvironment"
+ v-if="shouldShowExternalUrlButton"
data-testid="metrics-button"
:href="metricsPath"
:title="$options.i18n.metricsButtonTitle"
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 61438872afc..f1c728b84fd 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -23,11 +23,6 @@ export default {
required: true,
default: () => [],
},
- canReadEnvironment: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
@@ -155,7 +150,6 @@ export default {
<environment-item
:key="`environment-item-${i}`"
:model="model"
- :can-read-environment="canReadEnvironment"
:table-data="tableData"
data-qa-selector="environment_item"
/>
@@ -191,7 +185,6 @@ export default {
<environment-item
:key="`environment-row-${i}-${index}`"
:model="child"
- :can-read-environment="canReadEnvironment"
:table-data="tableData"
data-qa-selector="environment_item"
/>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index a02e72dfa72..6d427bef4e6 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -38,5 +38,3 @@ export const CANARY_STATUS = {
};
export const CANARY_UPDATE_MODAL = 'confirm-canary-change';
-
-export const ENVIRONMENTS_SURVEY_DISMISSED_COOKIE_NAME = 'environments_survey_alert_dismissed';
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 1be9a4608cb..206381e0b7e 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { parseBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
import environmentsFolderApp from './environments_folder_view.vue';
@@ -31,7 +30,6 @@ export default () => {
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
cssContainerClass: environmentsData.cssClass,
- canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment),
};
},
render(createElement) {
@@ -40,7 +38,6 @@ export default () => {
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
- canReadEnvironment: this.canReadEnvironment,
},
});
},
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index 8070f3f12f8..3c608ad0ba9 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -30,10 +30,6 @@ export default {
required: false,
default: '',
},
- canReadEnvironment: {
- type: Boolean,
- required: true,
- },
},
methods: {
successCallback(resp) {
@@ -72,7 +68,6 @@ export default {
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
- :can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
</div>
diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js
index b99872f7a6c..5e33923d518 100644
--- a/app/assets/javascripts/environments/index.js
+++ b/app/assets/javascripts/environments/index.js
@@ -9,7 +9,7 @@ Vue.use(Translate);
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
export default () => {
@@ -32,7 +32,6 @@ export default () => {
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
canCreateEnvironment: parseBoolean(environmentsData.canCreateEnvironment),
- canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
@@ -42,7 +41,6 @@ export default () => {
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
canCreateEnvironment: this.canCreateEnvironment,
- canReadEnvironment: this.canReadEnvironment,
},
});
},
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index f1c2dfec94b..6df4fad83f2 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -36,7 +36,6 @@ export const initHeader = () => {
environment: this.environment,
canDestroyEnvironment: dataset.canDestroyEnvironment,
canUpdateEnvironment: dataset.canUpdateEnvironment,
- canReadEnvironment: dataset.canReadEnvironment,
canStopEnvironment: dataset.canStopEnvironment,
canAdminEnvironment: dataset.canAdminEnvironment,
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 2e27f51b71f..5db8c8cf8d3 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -118,7 +118,7 @@ export default {
required: true,
},
},
- hasLocalStorage: AccessorUtils.isLocalStorageAccessSafe(),
+ hasLocalStorage: AccessorUtils.canUseLocalStorage(),
data() {
return {
errorSearchQuery: '',
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index 68b4438831e..2b8a31da50f 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -132,7 +131,7 @@ export default {
<td
class="line_content"
:class="{ old: isHighlighted(lineNum(line)) }"
- v-html="lineCode(line)"
+ v-html="lineCode(line) /* eslint-disable-line vue/no-v-html */"
></td>
</tr>
</template>
diff --git a/app/assets/javascripts/error_tracking/store/list/mutations.js b/app/assets/javascripts/error_tracking/store/list/mutations.js
index d92a64947ad..523861363d7 100644
--- a/app/assets/javascripts/error_tracking/store/list/mutations.js
+++ b/app/assets/javascripts/error_tracking/store/list/mutations.js
@@ -22,7 +22,7 @@ export default {
// only keep the last 5
state.recentSearches = recentSearches.slice(0, 5);
- if (AccessorUtils.isLocalStorageAccessSafe()) {
+ if (AccessorUtils.canUseLocalStorage()) {
localStorage.setItem(
`recent-searches${state.indexPath}`,
JSON.stringify(state.recentSearches),
@@ -31,7 +31,7 @@ export default {
},
[types.CLEAR_RECENT_SEARCHES](state) {
state.recentSearches = [];
- if (AccessorUtils.isLocalStorageAccessSafe()) {
+ if (AccessorUtils.canUseLocalStorage()) {
localStorage.removeItem(`recent-searches${state.indexPath}`);
}
},
diff --git a/app/assets/javascripts/error_tracking_settings/components/app.vue b/app/assets/javascripts/error_tracking_settings/components/app.vue
index d188574e721..e12d9cc2b07 100644
--- a/app/assets/javascripts/error_tracking_settings/components/app.vue
+++ b/app/assets/javascripts/error_tracking_settings/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
+import { GlButton, GlFormGroup, GlFormCheckbox, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import ErrorTrackingForm from './error_tracking_form.vue';
import ProjectDropdown from './project_dropdown.vue';
@@ -10,6 +10,8 @@ export default {
GlButton,
GlFormCheckbox,
GlFormGroup,
+ GlFormRadioGroup,
+ GlFormRadio,
ProjectDropdown,
},
props: {
@@ -22,6 +24,10 @@ export default {
type: String,
required: true,
},
+ initialIntegrated: {
+ type: String,
+ required: true,
+ },
initialProject: {
type: String,
required: false,
@@ -49,12 +55,20 @@ export default {
'isProjectInvalid',
'projectSelectionLabel',
]),
- ...mapState(['enabled', 'projects', 'selectedProject', 'settingsLoading', 'token']),
+ ...mapState([
+ 'enabled',
+ 'integrated',
+ 'projects',
+ 'selectedProject',
+ 'settingsLoading',
+ 'token',
+ ]),
},
created() {
this.setInitialState({
apiHost: this.initialApiHost,
enabled: this.initialEnabled,
+ integrated: this.initialIntegrated,
project: this.initialProject,
token: this.initialToken,
listProjectsEndpoint: this.listProjectsEndpoint,
@@ -62,7 +76,13 @@ export default {
});
},
methods: {
- ...mapActions(['setInitialState', 'updateEnabled', 'updateSelectedProject', 'updateSettings']),
+ ...mapActions([
+ 'setInitialState',
+ 'updateEnabled',
+ 'updateIntegrated',
+ 'updateSelectedProject',
+ 'updateSettings',
+ ]),
handleSubmit() {
this.updateSettings();
},
@@ -76,27 +96,44 @@ export default {
:label="s__('ErrorTracking|Enable error tracking')"
label-for="error-tracking-enabled"
>
- <gl-form-checkbox
- id="error-tracking-enabled"
- :checked="enabled"
- @change="updateEnabled($event)"
- >
+ <gl-form-checkbox id="error-tracking-enabled" :checked="enabled" @change="updateEnabled">
{{ s__('ErrorTracking|Active') }}
</gl-form-checkbox>
</gl-form-group>
- <error-tracking-form />
- <div class="form-group">
- <project-dropdown
- :has-projects="hasProjects"
- :invalid-project-label="invalidProjectLabel"
- :is-project-invalid="isProjectInvalid"
- :dropdown-label="dropdownLabel"
- :project-selection-label="projectSelectionLabel"
- :projects="projects"
- :selected-project="selectedProject"
- :token="token"
- @select-project="updateSelectedProject"
- />
+ <gl-form-group
+ :label="s__('ErrorTracking|Error tracking backend')"
+ data-testid="tracking-backend-settings"
+ >
+ <gl-form-radio-group name="explicit" :checked="integrated" @change="updateIntegrated">
+ <gl-form-radio name="error-tracking-integrated" :value="false">
+ {{ __('Sentry') }}
+ <template #help>
+ {{ __('Requires you to deploy or set up cloud-hosted Sentry.') }}
+ </template>
+ </gl-form-radio>
+ <gl-form-radio name="error-tracking-integrated" :value="true">
+ {{ __('GitLab') }}
+ <template #help>
+ {{ __('Uses GitLab as a lightweight alternative to Sentry.') }}
+ </template>
+ </gl-form-radio>
+ </gl-form-radio-group>
+ </gl-form-group>
+ <div v-if="!integrated" class="js-sentry-setting-form" data-testid="sentry-setting-form">
+ <error-tracking-form />
+ <div class="form-group">
+ <project-dropdown
+ :has-projects="hasProjects"
+ :invalid-project-label="invalidProjectLabel"
+ :is-project-invalid="isProjectInvalid"
+ :dropdown-label="dropdownLabel"
+ :project-selection-label="projectSelectionLabel"
+ :projects="projects"
+ :selected-project="selectedProject"
+ :token="token"
+ @select-project="updateSelectedProject"
+ />
+ </div>
</div>
<gl-button
:disabled="settingsLoading"
diff --git a/app/assets/javascripts/error_tracking_settings/index.js b/app/assets/javascripts/error_tracking_settings/index.js
index ce315963723..324b3292834 100644
--- a/app/assets/javascripts/error_tracking_settings/index.js
+++ b/app/assets/javascripts/error_tracking_settings/index.js
@@ -5,7 +5,15 @@ import createStore from './store';
export default () => {
const formContainerEl = document.querySelector('.js-error-tracking-form');
const {
- dataset: { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
+ dataset: {
+ apiHost,
+ enabled,
+ integrated,
+ project,
+ token,
+ listProjectsEndpoint,
+ operationsSettingsEndpoint,
+ },
} = formContainerEl;
return new Vue({
@@ -16,6 +24,7 @@ export default () => {
props: {
initialApiHost: apiHost,
initialEnabled: enabled,
+ initialIntegrated: integrated,
initialProject: project,
initialToken: token,
listProjectsEndpoint,
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index d402d0336d9..972ad58c617 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -79,6 +79,10 @@ export const updateEnabled = ({ commit }, enabled) => {
commit(types.UPDATE_ENABLED, enabled);
};
+export const updateIntegrated = ({ commit }, integrated) => {
+ commit(types.UPDATE_INTEGRATED, integrated);
+};
+
export const updateToken = ({ commit }, token) => {
commit(types.UPDATE_TOKEN, token);
commit(types.RESET_CONNECT);
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutation_types.js b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
index bf3df383ddc..2cfa14c9b64 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutation_types.js
@@ -6,6 +6,7 @@ export const UPDATE_API_HOST = 'UPDATE_API_HOST';
export const UPDATE_CONNECT_ERROR = 'UPDATE_CONNECT_ERROR';
export const UPDATE_CONNECT_SUCCESS = 'UPDATE_CONNECT_SUCCESS';
export const UPDATE_ENABLED = 'UPDATE_ENABLED';
+export const UPDATE_INTEGRATED = 'UPDATE_INTEGRATED';
export const UPDATE_SELECTED_PROJECT = 'UPDATE_SELECTED_PROJECT';
export const UPDATE_SETTINGS_LOADING = 'UPDATE_SETTINGS_LOADING';
export const UPDATE_TOKEN = 'UPDATE_TOKEN';
diff --git a/app/assets/javascripts/error_tracking_settings/store/mutations.js b/app/assets/javascripts/error_tracking_settings/store/mutations.js
index 2242169aa1e..a1b43ccaaee 100644
--- a/app/assets/javascripts/error_tracking_settings/store/mutations.js
+++ b/app/assets/javascripts/error_tracking_settings/store/mutations.js
@@ -20,9 +20,18 @@ export default {
},
[types.SET_INITIAL_STATE](
state,
- { apiHost, enabled, project, token, listProjectsEndpoint, operationsSettingsEndpoint },
+ {
+ apiHost,
+ enabled,
+ integrated,
+ project,
+ token,
+ listProjectsEndpoint,
+ operationsSettingsEndpoint,
+ },
) {
state.enabled = parseBoolean(enabled);
+ state.integrated = parseBoolean(integrated);
state.apiHost = apiHost;
state.token = token;
state.listProjectsEndpoint = listProjectsEndpoint;
@@ -38,6 +47,9 @@ export default {
[types.UPDATE_ENABLED](state, enabled) {
state.enabled = enabled;
},
+ [types.UPDATE_INTEGRATED](state, integrated) {
+ state.integrated = integrated;
+ },
[types.UPDATE_TOKEN](state, token) {
state.token = token;
},
diff --git a/app/assets/javascripts/error_tracking_settings/store/state.js b/app/assets/javascripts/error_tracking_settings/store/state.js
index ab616f11e83..ee5597abeb3 100644
--- a/app/assets/javascripts/error_tracking_settings/store/state.js
+++ b/app/assets/javascripts/error_tracking_settings/store/state.js
@@ -1,6 +1,7 @@
export default () => ({
apiHost: '',
enabled: false,
+ integrated: false,
token: '',
projects: [],
isLoadingProjects: false,
diff --git a/app/assets/javascripts/error_tracking_settings/utils.js b/app/assets/javascripts/error_tracking_settings/utils.js
index 5d18ac8e802..7ef5f7bbd34 100644
--- a/app/assets/javascripts/error_tracking_settings/utils.js
+++ b/app/assets/javascripts/error_tracking_settings/utils.js
@@ -1,6 +1,12 @@
export const projectKeys = ['name', 'organizationName', 'organizationSlug', 'slug'];
-export const transformFrontendSettings = ({ apiHost, enabled, token, selectedProject }) => {
+export const transformFrontendSettings = ({
+ apiHost,
+ enabled,
+ integrated,
+ token,
+ selectedProject,
+}) => {
const project = selectedProject
? {
slug: selectedProject.slug,
@@ -10,7 +16,7 @@ export const transformFrontendSettings = ({ apiHost, enabled, token, selectedPro
}
: null;
- return { api_host: apiHost || null, enabled, token: token || null, project };
+ return { api_host: apiHost || null, enabled, integrated, token: token || null, project };
};
export const getDisplayName = (project) => `${project.organizationName} | ${project.slug}`;
diff --git a/app/assets/javascripts/experimentation/utils.js b/app/assets/javascripts/experimentation/utils.js
index e572280a62c..9079c238169 100644
--- a/app/assets/javascripts/experimentation/utils.js
+++ b/app/assets/javascripts/experimentation/utils.js
@@ -1,18 +1,27 @@
// This file only applies to use of experiments through https://gitlab.com/gitlab-org/gitlab-experiment
-import { get } from 'lodash';
+import { get, pick } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
+function getExperimentsData() {
+ return get(window, ['gon', 'experiment'], {});
+}
+
+function convertExperimentDataToExperimentContext(experimentData) {
+ return { schema: TRACKING_CONTEXT_SCHEMA, data: experimentData };
+}
+
export function getExperimentData(experimentName) {
- return get(window, ['gon', 'experiment', experimentName]);
+ return getExperimentsData()[experimentName];
}
export function getExperimentContexts(...experimentNames) {
- return experimentNames
- .map((name) => {
- const data = getExperimentData(name);
- return data && { schema: TRACKING_CONTEXT_SCHEMA, data };
- })
- .filter((context) => context);
+ return Object.values(pick(getExperimentsData(), experimentNames)).map(
+ convertExperimentDataToExperimentContext,
+ );
+}
+
+export function getAllExperimentContexts() {
+ return Object.values(getExperimentsData()).map(convertExperimentDataToExperimentContext);
}
export function isExperimentVariant(experimentName, variantName) {
diff --git a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
index dde021b67be..05d557db942 100644
--- a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
@@ -48,7 +48,7 @@ export default {
<gl-toggle
:value="active"
data-testid="feature-flag-status-toggle"
- data-track-event="click_button"
+ data-track-action="click_button"
data-track-label="feature_flag_toggle"
class="gl-mr-4"
:label="__('Feature flag status')"
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
index cfd838bf5a1..f8a8bed2467 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -115,7 +115,7 @@ export default {
:label="$options.i18n.toggleLabel"
label-position="hidden"
data-testid="feature-flag-status-toggle"
- data-track-event="click_button"
+ data-track-action="click_button"
data-track-label="feature_flag_toggle"
@change="toggleFeatureFlag(featureFlag)"
/>
diff --git a/app/assets/javascripts/filtered_search/services/recent_searches_service.js b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
index 56824977a43..c3514198ad9 100644
--- a/app/assets/javascripts/filtered_search/services/recent_searches_service.js
+++ b/app/assets/javascripts/filtered_search/services/recent_searches_service.js
@@ -33,7 +33,7 @@ class RecentSearchesService {
}
static isAvailable() {
- return AccessorUtilities.isLocalStorageAccessSafe();
+ return AccessorUtilities.canUseLocalStorage();
}
}
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index dd405893e43..8ad9eeaa266 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -84,7 +84,7 @@ export default {
logItemAccess(storageKey, unsanitizedItem) {
const item = sanitizeItem(unsanitizedItem);
- if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (!AccessorUtilities.canUseLocalStorage()) {
return false;
}
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 1137951ccfc..2f451e8353b 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable vue/require-default-prop, vue/no-v-html */
+/* eslint-disable vue/require-default-prop */
import { GlButton } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -75,7 +75,7 @@ export default {
ref="frequentItemsItemTitle"
:title="itemName"
class="frequent-items-item-title"
- v-html="highlightedItemName"
+ v-html="highlightedItemName /* eslint-disable-line vue/no-v-html */"
></div>
<div
v-if="namespace"
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index 65a762f54ad..babc2ef2e32 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -25,7 +25,7 @@ export const receiveFrequentItemsError = ({ commit }) => {
export const fetchFrequentItems = ({ state, dispatch }) => {
dispatch('requestFrequentItems');
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
const storedFrequentItems = JSON.parse(localStorage.getItem(state.storageKey));
dispatch(
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 312dd0c88dd..692de9dcb88 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -1,3 +1,5 @@
+export const MINIMUM_SEARCH_LENGTH = 3;
+
export const TYPE_CI_RUNNER = 'Ci::Runner';
export const TYPE_EPIC = 'Epic';
export const TYPE_GROUP = 'Group';
@@ -11,3 +13,5 @@ export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
export const TYPE_USER = 'User';
export const TYPE_VULNERABILITY = 'Vulnerability';
+export const TYPE_NOTE = 'Note';
+export const TYPE_DISCUSSION = 'Discussion';
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 2a95b242510..a1ec5942d64 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -136,7 +136,7 @@ export default {
this.updateGroups(res, Boolean(filterGroupsBy));
});
},
- fetchPage(page, filterGroupsBy, sortBy, archived) {
+ fetchPage({ page, filterGroupsBy, sortBy, archived }) {
this.isLoading = true;
return this.fetchGroups({
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 59a37b2a1d5..18a6d487703 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -32,10 +32,10 @@ export default {
},
methods: {
change(page) {
- const filterGroupsParam = getParameterByName('filter');
- const sortParam = getParameterByName('sort');
- const archivedParam = getParameterByName('archived');
- eventHub.$emit(`${this.action}fetchPage`, page, filterGroupsParam, sortParam, archivedParam);
+ const filterGroupsBy = getParameterByName('filter');
+ const sortBy = getParameterByName('sort');
+ const archived = getParameterByName('archived');
+ eventHub.$emit(`${this.action}fetchPage`, { page, filterGroupsBy, sortBy, archived });
},
},
};
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
index 402d9a07c53..dfc1549fb4a 100644
--- a/app/assets/javascripts/groups/components/invite_members_banner.vue
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -1,7 +1,7 @@
<script>
import { GlBanner } from '@gitlab/ui';
import eventHub from '~/invite_members/event_hub';
-import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
+import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
@@ -12,10 +12,10 @@ export default {
GlBanner,
},
mixins: [trackingMixin],
- inject: ['svgPath', 'isDismissedKey', 'trackLabel'],
+ inject: ['svgPath', 'trackLabel', 'calloutsPath', 'calloutsFeatureId', 'groupId'],
data() {
return {
- isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
+ isDismissed: false,
tracking: {
label: this.trackLabel,
},
@@ -26,7 +26,16 @@ export default {
},
methods: {
handleClose() {
- setCookie(this.isDismissedKey, true);
+ axios
+ .post(this.calloutsPath, {
+ feature_name: this.calloutsFeatureId,
+ group_id: this.groupId,
+ })
+ .catch((e) => {
+ // eslint-disable-next-line @gitlab/require-i18n-strings, no-console
+ console.error('Failed to dismiss banner.', e);
+ });
+
this.isDismissed = true;
this.track(this.$options.dismissEvent);
},
@@ -61,6 +70,7 @@ export default {
<gl-banner
v-if="!isDismissed"
ref="banner"
+ data-testid="invite-members-banner"
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
:svg-path="svgPath"
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 7a37d1eb93d..46e9d2bec99 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -40,24 +40,31 @@ export default {
return this.item.type === ITEM_TYPE.GROUP;
},
},
+ methods: {
+ displayValue(value) {
+ return this.isGroup && value !== undefined;
+ },
+ },
};
</script>
<template>
<div class="stats gl-text-gray-500">
<item-stats-value
- v-if="isGroup"
+ v-if="displayValue(item.subgroupCount)"
:title="__('Subgroups')"
:value="item.subgroupCount"
css-class="number-subgroups gl-ml-5"
icon-name="folder-o"
+ data-testid="subgroups-count"
/>
<item-stats-value
- v-if="isGroup"
+ v-if="displayValue(item.projectCount)"
:title="__('Projects')"
:value="item.projectCount"
css-class="number-projects gl-ml-5"
icon-name="bookmark"
+ data-testid="projects-count"
/>
<item-stats-value
v-if="isGroup"
diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js
index 2052dd6ac8c..38ab4122dab 100644
--- a/app/assets/javascripts/groups/init_invite_members_banner.js
+++ b/app/assets/javascripts/groups/init_invite_members_banner.js
@@ -8,15 +8,24 @@ export default function initInviteMembersBanner() {
return false;
}
- const { svgPath, inviteMembersPath, isDismissedKey, trackLabel } = el.dataset;
+ const {
+ svgPath,
+ inviteMembersPath,
+ trackLabel,
+ calloutsPath,
+ calloutsFeatureId,
+ groupId,
+ } = el.dataset;
return new Vue({
el,
provide: {
svgPath,
inviteMembersPath,
- isDismissedKey,
trackLabel,
+ calloutsPath,
+ calloutsFeatureId,
+ groupId,
},
render: (createElement) => createElement(InviteMembersBanner),
});
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
new file mode 100644
index 00000000000..580c27f6c61
--- /dev/null
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -0,0 +1,83 @@
+<script>
+import { GlSearchBoxByType, GlOutsideDirective as Outside } from '@gitlab/ui';
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import HeaderSearchDefaultItems from './header_search_default_items.vue';
+import HeaderSearchScopedItems from './header_search_scoped_items.vue';
+
+export default {
+ name: 'HeaderSearchApp',
+ i18n: {
+ searchPlaceholder: __('Search or jump to...'),
+ },
+ directives: { Outside },
+ components: {
+ GlSearchBoxByType,
+ HeaderSearchDefaultItems,
+ HeaderSearchScopedItems,
+ },
+ data() {
+ return {
+ showDropdown: false,
+ };
+ },
+ computed: {
+ ...mapState(['search']),
+ ...mapGetters(['searchQuery']),
+ searchText: {
+ get() {
+ return this.search;
+ },
+ set(value) {
+ this.setSearch(value);
+ },
+ },
+ showSearchDropdown() {
+ return this.showDropdown && gon?.current_username;
+ },
+ showDefaultItems() {
+ return !this.searchText;
+ },
+ },
+ methods: {
+ ...mapActions(['setSearch']),
+ openDropdown() {
+ this.showDropdown = true;
+ },
+ closeDropdown() {
+ this.showDropdown = false;
+ },
+ submitSearch() {
+ return visitUrl(this.searchQuery);
+ },
+ },
+};
+</script>
+
+<template>
+ <section v-outside="closeDropdown" class="header-search gl-relative">
+ <gl-search-box-by-type
+ v-model="searchText"
+ :debounce="500"
+ autocomplete="off"
+ :placeholder="$options.i18n.searchPlaceholder"
+ @focus="openDropdown"
+ @click="openDropdown"
+ @keydown.enter="submitSearch"
+ @keydown.esc="closeDropdown"
+ />
+ <div
+ v-if="showSearchDropdown"
+ data-testid="header-search-dropdown-menu"
+ class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-left-0 gl-z-index-1 gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0"
+ >
+ <div class="header-search-dropdown-content gl-overflow-y-auto gl-py-2">
+ <header-search-default-items v-if="showDefaultItems" />
+ <template v-else>
+ <header-search-scoped-items />
+ </template>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_default_items.vue b/app/assets/javascripts/header_search/components/header_search_default_items.vue
new file mode 100644
index 00000000000..2871937ed3a
--- /dev/null
+++ b/app/assets/javascripts/header_search/components/header_search_default_items.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
+import { __ } from '~/locale';
+
+export default {
+ name: 'HeaderSearchDefaultItems',
+ i18n: {
+ allGitLab: __('All GitLab'),
+ },
+ components: {
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ },
+ computed: {
+ ...mapState(['searchContext']),
+ ...mapGetters(['defaultSearchOptions']),
+ sectionHeader() {
+ return (
+ this.searchContext.project?.name ||
+ this.searchContext.group?.name ||
+ this.$options.i18n.allGitLab
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="(option, index) in defaultSearchOptions"
+ :id="`default-${index}`"
+ :key="index"
+ tabindex="-1"
+ :href="option.url"
+ >
+ {{ option.title }}
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
new file mode 100644
index 00000000000..645eba05148
--- /dev/null
+++ b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
+
+export default {
+ name: 'HeaderSearchScopedItems',
+ components: {
+ GlDropdownItem,
+ },
+ computed: {
+ ...mapState(['search']),
+ ...mapGetters(['scopedSearchOptions']),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-dropdown-item
+ v-for="(option, index) in scopedSearchOptions"
+ :id="`scoped-${index}`"
+ :key="index"
+ tabindex="-1"
+ :href="option.url"
+ >
+ "<span class="gl-font-weight-bold">{{ search }}</span
+ >" {{ option.description }}
+ <span v-if="option.scope" class="gl-font-style-italic">{{ option.scope }}</span>
+ </gl-dropdown-item>
+ </div>
+</template>
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
new file mode 100644
index 00000000000..fffed7bcbdb
--- /dev/null
+++ b/app/assets/javascripts/header_search/constants.js
@@ -0,0 +1,17 @@
+import { __ } from '~/locale';
+
+export const MSG_ISSUES_ASSIGNED_TO_ME = __('Issues assigned to me');
+
+export const MSG_ISSUES_IVE_CREATED = __("Issues I've created");
+
+export const MSG_MR_ASSIGNED_TO_ME = __('Merge requests assigned to me');
+
+export const MSG_MR_IM_REVIEWER = __("Merge requests that I'm a reviewer");
+
+export const MSG_MR_IVE_CREATED = __("Merge requests I've created");
+
+export const MSG_IN_ALL_GITLAB = __('in all GitLab');
+
+export const MSG_IN_GROUP = __('in group');
+
+export const MSG_IN_PROJECT = __('in project');
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
new file mode 100644
index 00000000000..2d37ee137fc
--- /dev/null
+++ b/app/assets/javascripts/header_search/index.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import HeaderSearchApp from './components/app.vue';
+import createStore from './store';
+
+Vue.use(Translate);
+
+export const initHeaderSearchApp = () => {
+ const el = document.getElementById('js-header-search');
+
+ if (!el) {
+ return false;
+ }
+
+ const { searchPath, issuesPath, mrPath } = el.dataset;
+ let { searchContext } = el.dataset;
+ searchContext = JSON.parse(searchContext);
+
+ return new Vue({
+ el,
+ store: createStore({ searchPath, issuesPath, mrPath, searchContext }),
+ render(createElement) {
+ return createElement(HeaderSearchApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/header_search/store/actions.js b/app/assets/javascripts/header_search/store/actions.js
new file mode 100644
index 00000000000..841aee04029
--- /dev/null
+++ b/app/assets/javascripts/header_search/store/actions.js
@@ -0,0 +1,5 @@
+import * as types from './mutation_types';
+
+export const setSearch = ({ commit }, value) => {
+ commit(types.SET_SEARCH, value);
+};
diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js
new file mode 100644
index 00000000000..d1e1fc8ad73
--- /dev/null
+++ b/app/assets/javascripts/header_search/store/getters.js
@@ -0,0 +1,135 @@
+import { objectToQuery } from '~/lib/utils/url_utility';
+
+import {
+ MSG_ISSUES_ASSIGNED_TO_ME,
+ MSG_ISSUES_IVE_CREATED,
+ MSG_MR_ASSIGNED_TO_ME,
+ MSG_MR_IM_REVIEWER,
+ MSG_MR_IVE_CREATED,
+ MSG_IN_PROJECT,
+ MSG_IN_GROUP,
+ MSG_IN_ALL_GITLAB,
+} from '../constants';
+
+export const searchQuery = (state) => {
+ const query = {
+ search: state.search,
+ nav_source: 'navbar',
+ project_id: state.searchContext.project?.id,
+ group_id: state.searchContext.group?.id,
+ scope: state.searchContext.scope,
+ };
+
+ return `${state.searchPath}?${objectToQuery(query)}`;
+};
+
+export const scopedIssuesPath = (state) => {
+ return (
+ state.searchContext.project_metadata?.issues_path ||
+ state.searchContext.group_metadata?.issues_path ||
+ state.issuesPath
+ );
+};
+
+export const scopedMRPath = (state) => {
+ return (
+ state.searchContext.project_metadata?.mr_path ||
+ state.searchContext.group_metadata?.mr_path ||
+ state.mrPath
+ );
+};
+
+export const defaultSearchOptions = (state, getters) => {
+ const userName = gon.current_username;
+
+ return [
+ {
+ title: MSG_ISSUES_ASSIGNED_TO_ME,
+ url: `${getters.scopedIssuesPath}/?assignee_username=${userName}`,
+ },
+ {
+ title: MSG_ISSUES_IVE_CREATED,
+ url: `${getters.scopedIssuesPath}/?author_username=${userName}`,
+ },
+ {
+ title: MSG_MR_ASSIGNED_TO_ME,
+ url: `${getters.scopedMRPath}/?assignee_username=${userName}`,
+ },
+ {
+ title: MSG_MR_IM_REVIEWER,
+ url: `${getters.scopedMRPath}/?reviewer_username=${userName}`,
+ },
+ {
+ title: MSG_MR_IVE_CREATED,
+ url: `${getters.scopedMRPath}/?author_username=${userName}`,
+ },
+ ];
+};
+
+export const projectUrl = (state) => {
+ if (!state.searchContext.project || !state.searchContext.group) {
+ return null;
+ }
+
+ const query = {
+ search: state.search,
+ nav_source: 'navbar',
+ project_id: state.searchContext.project.id,
+ group_id: state.searchContext.group.id,
+ scope: state.searchContext.scope,
+ };
+
+ return `${state.searchPath}?${objectToQuery(query)}`;
+};
+
+export const groupUrl = (state) => {
+ if (!state.searchContext.group) {
+ return null;
+ }
+
+ const query = {
+ search: state.search,
+ nav_source: 'navbar',
+ group_id: state.searchContext.group.id,
+ scope: state.searchContext.scope,
+ };
+
+ return `${state.searchPath}?${objectToQuery(query)}`;
+};
+
+export const allUrl = (state) => {
+ const query = {
+ search: state.search,
+ nav_source: 'navbar',
+ scope: state.searchContext.scope,
+ };
+
+ return `${state.searchPath}?${objectToQuery(query)}`;
+};
+
+export const scopedSearchOptions = (state, getters) => {
+ const options = [];
+
+ if (state.searchContext.project) {
+ options.push({
+ scope: state.searchContext.project.name,
+ description: MSG_IN_PROJECT,
+ url: getters.projectUrl,
+ });
+ }
+
+ if (state.searchContext.group) {
+ options.push({
+ scope: state.searchContext.group.name,
+ description: MSG_IN_GROUP,
+ url: getters.groupUrl,
+ });
+ }
+
+ options.push({
+ description: MSG_IN_ALL_GITLAB,
+ url: getters.allUrl,
+ });
+
+ return options;
+};
diff --git a/app/assets/javascripts/header_search/store/index.js b/app/assets/javascripts/header_search/store/index.js
new file mode 100644
index 00000000000..8b74f8662a5
--- /dev/null
+++ b/app/assets/javascripts/header_search/store/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import createState from './state';
+
+Vue.use(Vuex);
+
+export const getStoreConfig = ({ searchPath, issuesPath, mrPath, searchContext }) => ({
+ actions,
+ getters,
+ mutations,
+ state: createState({ searchPath, issuesPath, mrPath, searchContext }),
+});
+
+const createStore = (config) => new Vuex.Store(getStoreConfig(config));
+export default createStore;
diff --git a/app/assets/javascripts/header_search/store/mutation_types.js b/app/assets/javascripts/header_search/store/mutation_types.js
new file mode 100644
index 00000000000..0bc94ae055f
--- /dev/null
+++ b/app/assets/javascripts/header_search/store/mutation_types.js
@@ -0,0 +1 @@
+export const SET_SEARCH = 'SET_SEARCH';
diff --git a/app/assets/javascripts/header_search/store/mutations.js b/app/assets/javascripts/header_search/store/mutations.js
new file mode 100644
index 00000000000..5b1438929d4
--- /dev/null
+++ b/app/assets/javascripts/header_search/store/mutations.js
@@ -0,0 +1,7 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_SEARCH](state, value) {
+ state.search = value;
+ },
+};
diff --git a/app/assets/javascripts/header_search/store/state.js b/app/assets/javascripts/header_search/store/state.js
new file mode 100644
index 00000000000..fb2c83dbbe3
--- /dev/null
+++ b/app/assets/javascripts/header_search/store/state.js
@@ -0,0 +1,8 @@
+const createState = ({ searchPath, issuesPath, mrPath, searchContext }) => ({
+ searchPath,
+ issuesPath,
+ mrPath,
+ searchContext,
+ search: '',
+});
+export default createState;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
index 977efb0ca22..5a7d7917f8a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { mapState } from 'vuex';
export default {
@@ -17,7 +16,7 @@ export default {
<div class="gl-mr-3 gl-ml-3">
<div class="text-content text-center">
<h4>{{ __('All changes are committed') }}</h4>
- <p v-html="lastCommitMsg"></p>
+ <p v-html="lastCommitMsg /* eslint-disable-line vue/no-v-html */"></p>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index 2b75d10f659..67eedc6b37f 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
export default {
@@ -8,6 +7,9 @@ export default {
GlAlert,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
message: {
type: Object,
@@ -56,7 +58,7 @@ export default {
@dismiss="dismiss"
@primaryAction="doAction"
>
- <span v-html="message.text"></span>
+ <span v-safe-html="message.text"></span>
<gl-loading-icon v-show="isLoading" size="sm" inline class="vertical-align-middle ml-1" />
</gl-alert>
</template>
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 8e611503cb4..c142992a9d1 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
import { mapActions, mapState } from 'vuex';
@@ -102,7 +101,7 @@ export default {
<code
v-show="!detailJob.isLoading"
class="bash"
- v-html="jobOutput"
+ v-html="jobOutput /* eslint-disable-line vue/no-v-html */"
>
</code>
<div
diff --git a/app/assets/javascripts/ide/services/terminals.js b/app/assets/javascripts/ide/services/terminals.js
index ea54733baa4..99121948196 100644
--- a/app/assets/javascripts/ide/services/terminals.js
+++ b/app/assets/javascripts/ide/services/terminals.js
@@ -1,6 +1,8 @@
import axios from '~/lib/utils/axios_utils';
+import { joinPaths } from '~/lib/utils/url_utility';
-export const baseUrl = (projectPath) => `/${projectPath}/ide_terminals`;
+export const baseUrl = (projectPath) =>
+ joinPaths(gon.relative_url_root || '', `/${projectPath}/ide_terminals`);
export const checkConfig = (projectPath, branch) =>
axios.post(`${baseUrl(projectPath)}/check_config`, {
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index 275fecc5a32..ec3630cc5eb 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -43,7 +43,10 @@ const KNOWN_TYPES = [
},
];
-export function isTextFile({ name, raw, content, mimeType = '' }) {
+export function isTextFile({ name, raw, binary, content, mimeType = '' }) {
+ // some file objects already have a `binary` property set on them. If so, use it first
+ if (typeof binary === 'boolean') return !binary;
+
const knownType = KNOWN_TYPES.find((type) => type.isMatch(mimeType, name));
if (knownType) return knownType.isText;
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
new file mode 100644
index 00000000000..104c84173fc
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlButton, GlIcon, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { isFinished, isInvalid, isAvailableForImport } from '../utils';
+
+export default {
+ components: {
+ GlIcon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ groupPathRegex: {
+ type: RegExp,
+ required: true,
+ },
+ },
+ computed: {
+ fullLastImportPath() {
+ return this.group.last_import_target
+ ? `${this.group.last_import_target.target_namespace}/${this.group.last_import_target.new_name}`
+ : null;
+ },
+ absoluteLastImportPath() {
+ return joinPaths(gon.relative_url_root || '/', this.fullLastImportPath);
+ },
+ isAvailableForImport() {
+ return isAvailableForImport(this.group);
+ },
+ isFinished() {
+ return isFinished(this.group);
+ },
+ isInvalid() {
+ return isInvalid(this.group, this.groupPathRegex);
+ },
+ },
+};
+</script>
+
+<template>
+ <span class="gl-white-space-nowrap gl-inline-flex gl-align-items-center">
+ <gl-button
+ v-if="isAvailableForImport"
+ :disabled="isInvalid"
+ variant="confirm"
+ category="secondary"
+ data-qa-selector="import_group_button"
+ @click="$emit('import-group')"
+ >
+ {{ isFinished ? __('Re-import') : __('Import') }}
+ </gl-button>
+ <gl-icon
+ v-if="isFinished"
+ v-gl-tooltip
+ :size="16"
+ name="information-o"
+ :title="
+ s__('BulkImports|Re-import creates a new group. It does not sync with the existing group.')
+ "
+ class="gl-ml-3"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
new file mode 100644
index 00000000000..2de9bd4f868
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_source_cell.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlLink, GlSprintf, GlIcon } from '@gitlab/ui';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { isFinished } from '../utils';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ GlIcon,
+ },
+ props: {
+ group: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ fullLastImportPath() {
+ return this.group.last_import_target
+ ? `${this.group.last_import_target.target_namespace}/${this.group.last_import_target.new_name}`
+ : null;
+ },
+ absoluteLastImportPath() {
+ return joinPaths(gon.relative_url_root || '/', this.fullLastImportPath);
+ },
+ isFinished() {
+ return isFinished(this.group);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-link
+ :href="group.web_url"
+ target="_blank"
+ class="gl-display-inline-flex gl-align-items-center gl-h-7"
+ >
+ {{ group.full_path }} <gl-icon name="external-link" />
+ </gl-link>
+ <div v-if="isFinished && fullLastImportPath" class="gl-font-sm">
+ <gl-sprintf :message="s__('BulkImport|Last imported to %{link}')">
+ <template #link>
+ <gl-link :href="absoluteLastImportPath" class="gl-font-sm" target="_blank">{{
+ fullLastImportPath
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index db44be2bcd7..04b037ecc2b 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
@@ -9,19 +9,19 @@ import {
GlLoadingIcon,
GlSearchBoxByClick,
GlSprintf,
- GlSafeHtmlDirective as SafeHtml,
GlTable,
GlFormCheckbox,
} from '@gitlab/ui';
import { s__, __, n__ } from '~/locale';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import ImportStatus from '../../components/import_status.vue';
-import { STATUSES } from '../../constants';
+import ImportStatusCell from '../../components/import_status.vue';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
import setImportTargetMutation from '../graphql/mutations/set_import_target.mutation.graphql';
import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
-import { isInvalid } from '../utils';
+import { isInvalid, isFinished, isAvailableForImport } from '../utils';
+import ImportActionsCell from './import_actions_cell.vue';
+import ImportSourceCell from './import_source_cell.vue';
import ImportTargetCell from './import_target_cell.vue';
const PAGE_SIZES = [20, 50, 100];
@@ -43,13 +43,12 @@ export default {
GlFormCheckbox,
GlSprintf,
GlTable,
- ImportStatus,
+ ImportSourceCell,
ImportTargetCell,
+ ImportStatusCell,
+ ImportActionsCell,
PaginationLinks,
},
- directives: {
- SafeHtml,
- },
props: {
sourceUrl: {
@@ -136,7 +135,7 @@ export default {
},
availableGroupsForImport() {
- return this.groups.filter((g) => g.progress.status === STATUSES.NONE && !this.isInvalid(g));
+ return this.groups.filter((g) => isAvailableForImport(g) && !this.isInvalid(g));
},
humanizedTotal() {
@@ -190,6 +189,24 @@ export default {
},
methods: {
+ isUnselectable(group) {
+ return !this.isAvailableForImport(group) || this.isInvalid(group);
+ },
+
+ rowClasses(group) {
+ const DEFAULT_CLASSES = [
+ 'gl-border-gray-200',
+ 'gl-border-0',
+ 'gl-border-b-1',
+ 'gl-border-solid',
+ ];
+ const result = [...DEFAULT_CLASSES];
+ if (this.isUnselectable(group)) {
+ result.push('gl-cursor-default!');
+ }
+ return result;
+ },
+
qaRowAttributes(group, type) {
if (type === 'row') {
return {
@@ -201,10 +218,8 @@ export default {
return {};
},
- isAlreadyImported(group) {
- return group.progress.status !== STATUSES.NONE;
- },
-
+ isAvailableForImport,
+ isFinished,
isInvalid(group) {
return isInvalid(group, this.groupPathRegex);
},
@@ -253,7 +268,7 @@ export default {
const table = this.getTableRef();
this.groups.forEach((group, idx) => {
- if (table.isRowSelected(idx) && (this.isAlreadyImported(group) || this.isInvalid(group))) {
+ if (table.isRowSelected(idx) && this.isUnselectable(group)) {
table.unselectRow(idx);
}
});
@@ -291,7 +306,7 @@ export default {
<strong>{{ filter }}</strong>
</template>
<template #link>
- <gl-link class="gl-display-inline-block" :href="sourceUrl" target="_blank">
+ <gl-link :href="sourceUrl" target="_blank">
{{ sourceUrl }} <gl-icon name="external-link" class="vertical-align-middle" />
</gl-link>
</template>
@@ -338,7 +353,7 @@ export default {
ref="table"
class="gl-w-full"
data-qa-selector="import_table"
- tbody-tr-class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"
+ :tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
:items="groups"
:fields="$options.fields"
@@ -360,18 +375,12 @@ export default {
<gl-form-checkbox
class="gl-h-7 gl-pt-3"
:checked="rowSelected"
- :disabled="isAlreadyImported(group) || isInvalid(group)"
+ :disabled="!isAvailableForImport(group) || isInvalid(group)"
@change="rowSelected ? unselectRow() : selectRow()"
/>
</template>
- <template #cell(web_url)="{ value: web_url, item: { full_path } }">
- <gl-link
- :href="web_url"
- target="_blank"
- class="gl-display-inline-flex gl-align-items-center gl-h-7"
- >
- {{ full_path }} <gl-icon name="external-link" />
- </gl-link>
+ <template #cell(web_url)="{ item: group }">
+ <import-source-cell :group="group" />
</template>
<template #cell(import_target)="{ item: group }">
<import-target-cell
@@ -388,19 +397,14 @@ export default {
/>
</template>
<template #cell(progress)="{ value: { status } }">
- <import-status :status="status" class="gl-line-height-32" />
+ <import-status-cell :status="status" class="gl-line-height-32" />
</template>
<template #cell(actions)="{ item: group }">
- <gl-button
- v-if="!isAlreadyImported(group)"
- :disabled="isInvalid(group)"
- variant="confirm"
- category="secondary"
- data-qa-selector="import_group_button"
- @click="importGroups([group.id])"
- >
- {{ __('Import') }}
- </gl-button>
+ <import-actions-cell
+ :group="group"
+ :group-path-regex="groupPathRegex"
+ @import-group="importGroups([group.id])"
+ />
</template>
</gl-table>
<div v-if="hasGroups" class="gl-display-flex gl-mt-3 gl-align-items-center">
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index 7359d4f239e..daced740c94 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -3,14 +3,16 @@ import {
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
- GlLink,
GlFormInput,
} from '@gitlab/ui';
-import { joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
-import { STATUSES } from '../../constants';
-import { isInvalid, getInvalidNameValidationMessage, isNameValid } from '../utils';
+import {
+ isInvalid,
+ getInvalidNameValidationMessage,
+ isNameValid,
+ isAvailableForImport,
+} from '../utils';
export default {
components: {
@@ -18,7 +20,6 @@ export default {
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
- GlLink,
GlFormInput,
},
props: {
@@ -61,20 +62,8 @@ export default {
return isNameValid(this.group, this.groupPathRegex);
},
- isAlreadyImported() {
- return this.group.progress.status !== STATUSES.NONE;
- },
-
- isFinished() {
- return this.group.progress.status === STATUSES.FINISHED;
- },
-
- fullPath() {
- return `${this.importTarget.target_namespace}/${this.importTarget.new_name}`;
- },
-
- absolutePath() {
- return joinPaths(gon.relative_url_root || '/', this.fullPath);
+ isAvailableForImport() {
+ return isAvailableForImport(this.group);
},
},
@@ -85,25 +74,11 @@ export default {
</script>
<template>
- <gl-link
- v-if="isFinished"
- class="gl-display-inline-flex gl-align-items-center gl-h-7"
- :href="absolutePath"
- >
- {{ fullPath }}
- </gl-link>
-
- <div
- v-else
- class="gl-display-flex gl-align-items-stretch"
- :class="{
- disabled: isAlreadyImported,
- }"
- >
+ <div class="gl-display-flex gl-align-items-stretch">
<import-group-dropdown
#default="{ namespaces }"
:text="importTarget.target_namespace"
- :disabled="isAlreadyImported"
+ :disabled="!isAvailableForImport"
:namespaces="availableNamespaceNames"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
@@ -131,8 +106,8 @@ export default {
<div
class="gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-bg-gray-10"
:class="{
- 'gl-text-gray-400 gl-border-gray-100': isAlreadyImported,
- 'gl-border-gray-200': !isAlreadyImported,
+ 'gl-text-gray-400 gl-border-gray-100': !isAvailableForImport,
+ 'gl-border-gray-200': isAvailableForImport,
}"
>
/
@@ -141,11 +116,11 @@ export default {
<gl-form-input
class="gl-rounded-top-left-none gl-rounded-bottom-left-none"
:class="{
- 'gl-inset-border-1-gray-200!': !isAlreadyImported,
- 'gl-inset-border-1-gray-100!': isAlreadyImported,
- 'is-invalid': isInvalid && !isAlreadyImported,
+ 'gl-inset-border-1-gray-200!': isAvailableForImport,
+ 'gl-inset-border-1-gray-100!': !isAvailableForImport,
+ 'is-invalid': isInvalid && isAvailableForImport,
}"
- :disabled="isAlreadyImported"
+ :disabled="!isAvailableForImport"
:value="importTarget.new_name"
@input="$emit('update-new-name', $event)"
/>
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index 57188441158..c08cf909a00 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -5,10 +5,13 @@ import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { STATUSES } from '../../constants';
import { i18n, NEW_NAME_FIELD } from '../constants';
+import { isAvailableForImport } from '../utils';
import bulkImportSourceGroupItemFragment from './fragments/bulk_import_source_group_item.fragment.graphql';
+import bulkImportSourceGroupProgressFragment from './fragments/bulk_import_source_group_progress.fragment.graphql';
import addValidationErrorMutation from './mutations/add_validation_error.mutation.graphql';
import removeValidationErrorMutation from './mutations/remove_validation_error.mutation.graphql';
import setImportProgressMutation from './mutations/set_import_progress.mutation.graphql';
+import setImportTargetMutation from './mutations/set_import_target.mutation.graphql';
import updateImportStatusMutation from './mutations/update_import_status.mutation.graphql';
import availableNamespacesQuery from './queries/available_namespaces.query.graphql';
import bulkImportSourceGroupQuery from './queries/bulk_import_source_group.query.graphql';
@@ -34,6 +37,7 @@ function makeGroup(data) {
};
const NESTED_OBJECT_FIELDS = {
import_target: clientTypenames.BulkImportTarget,
+ last_import_target: clientTypenames.BulkImportTarget,
progress: clientTypenames.BulkImportProgress,
};
@@ -55,6 +59,7 @@ async function checkImportTargetIsValid({ client, newName, targetNamespace, sour
data: { existingGroup, existingProject },
} = await client.query({
query: groupAndProjectQuery,
+ fetchPolicy: 'no-cache',
variables: {
fullPath: `${targetNamespace}/${newName}`,
},
@@ -82,6 +87,7 @@ async function checkImportTargetIsValid({ client, newName, targetNamespace, sour
}
const localProgressId = (id) => `not-started-${id}`;
+const nextName = (name) => `${name}-1`;
export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGroupsManager }) {
const groupsManager = new GroupsManager({
@@ -140,17 +146,28 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
const { jobId, importState: cachedImportState } =
groupsManager.getImportStateFromStorageByGroupId(group.id) ?? {};
+ const status = cachedImportState?.status ?? STATUSES.NONE;
+
+ const importTarget =
+ status === STATUSES.FINISHED && cachedImportState.importTarget
+ ? {
+ target_namespace: cachedImportState.importTarget.target_namespace,
+ new_name: nextName(cachedImportState.importTarget.new_name),
+ }
+ : cachedImportState?.importTarget ?? {
+ new_name: group.full_path,
+ target_namespace: availableNamespaces[0]?.full_path ?? '',
+ };
+
return makeGroup({
...group,
validation_errors: [],
progress: {
id: jobId ?? localProgressId(group.id),
- status: cachedImportState?.status ?? STATUSES.NONE,
- },
- import_target: cachedImportState?.importTarget ?? {
- new_name: group.full_path,
- target_namespace: availableNamespaces[0]?.full_path ?? '',
+ status,
},
+ import_target: importTarget,
+ last_import_target: cachedImportState?.importTarget ?? null,
});
}),
pageInfo: {
@@ -161,7 +178,7 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
setTimeout(() => {
response.nodes.forEach((group) => {
- if (group.progress.status === STATUSES.NONE) {
+ if (isAvailableForImport(group)) {
checkImportTargetIsValid({
client,
newName: group.import_target.new_name,
@@ -193,32 +210,18 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
targetNamespace,
newName,
});
+
return makeGroup({
id: sourceGroupId,
import_target: {
target_namespace: targetNamespace,
new_name: newName,
+ id: sourceGroupId,
},
});
},
- setTargetNamespace: (_, { targetNamespace, sourceGroupId }) =>
- makeGroup({
- id: sourceGroupId,
- import_target: {
- target_namespace: targetNamespace,
- },
- }),
-
- setNewName: (_, { newName, sourceGroupId }) =>
- makeGroup({
- id: sourceGroupId,
- import_target: {
- new_name: newName,
- },
- }),
-
- async setImportProgress(_, { sourceGroupId, status, jobId }) {
+ async setImportProgress(_, { sourceGroupId, status, jobId, importTarget }) {
if (jobId) {
groupsManager.updateImportProgress(jobId, status);
}
@@ -229,16 +232,46 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
id: jobId ?? localProgressId(sourceGroupId),
status,
},
+ last_import_target: {
+ __typename: clientTypenames.BulkImportTarget,
+ ...importTarget,
+ },
});
},
- async updateImportStatus(_, { id, status }) {
- groupsManager.updateImportProgress(id, status);
+ async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) {
+ groupsManager.updateImportProgress(id, newStatus);
+
+ const progressItem = client.readFragment({
+ fragment: bulkImportSourceGroupProgressFragment,
+ fragmentName: 'BulkImportSourceGroupProgress',
+ id: getCacheKey({
+ __typename: clientTypenames.BulkImportProgress,
+ id,
+ }),
+ });
+
+ const isInProgress = Boolean(progressItem);
+ const { status: currentStatus } = progressItem ?? {};
+ if (newStatus === STATUSES.FINISHED && isInProgress && currentStatus !== newStatus) {
+ const groups = groupsManager.getImportedGroupsByJobId(id);
+
+ groups.forEach(async ({ id: groupId, importTarget }) => {
+ client.mutate({
+ mutation: setImportTargetMutation,
+ variables: {
+ sourceGroupId: groupId,
+ targetNamespace: importTarget.target_namespace,
+ newName: nextName(importTarget.new_name),
+ },
+ });
+ });
+ }
return {
__typename: clientTypenames.BulkImportProgress,
id,
- status,
+ status: newStatus,
};
},
@@ -327,10 +360,10 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
return { status: STATUSES.NONE };
})
.then((newStatus) =>
- sourceGroupIds.forEach((sourceGroupId) =>
+ sourceGroupIds.forEach((sourceGroupId, idx) =>
client.mutate({
mutation: setImportProgressMutation,
- variables: { sourceGroupId, ...newStatus },
+ variables: { sourceGroupId, ...newStatus, importTarget: groups[idx].import_target },
}),
),
)
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
index 47675cd1bd0..089340b3c48 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_item.fragment.graphql
@@ -12,6 +12,10 @@ fragment BulkImportSourceGroupItem on ClientBulkImportSourceGroup {
target_namespace
new_name
}
+ last_import_target {
+ target_namespace
+ new_name
+ }
validation_errors {
field
message
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
index 2ec1269932a..43301554de3 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/set_import_progress.mutation.graphql
@@ -1,9 +1,23 @@
-mutation setImportProgress($status: String!, $sourceGroupId: String!, $jobId: String) {
- setImportProgress(status: $status, sourceGroupId: $sourceGroupId, jobId: $jobId) @client {
+mutation setImportProgress(
+ $status: String!
+ $sourceGroupId: String!
+ $jobId: String
+ $importTarget: ImportTargetInput!
+) {
+ setImportProgress(
+ status: $status
+ sourceGroupId: $sourceGroupId
+ jobId: $jobId
+ importTarget: $importTarget
+ ) @client {
id
progress {
id
status
}
+ last_import_target {
+ target_namespace
+ new_name
+ }
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
index 97dbdbf518a..7caa37d9ad4 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js
@@ -35,15 +35,18 @@ export class SourceGroupsManager {
}
createImportState(importId, jobConfig) {
- this.importStates[this.getStorageKey(importId)] = {
+ this.importStates[importId] = {
status: jobConfig.status,
- groups: jobConfig.groups.map((g) => ({ importTarget: g.import_target, id: g.id })),
+ groups: jobConfig.groups.map((g) => ({
+ importTarget: { ...g.import_target },
+ id: g.id,
+ })),
};
this.saveImportStatesToStorage();
}
updateImportProgress(importId, status) {
- const currentState = this.importStates[this.getStorageKey(importId)];
+ const currentState = this.importStates[importId];
if (!currentState) {
return;
}
@@ -52,12 +55,15 @@ export class SourceGroupsManager {
this.saveImportStatesToStorage();
}
+ getImportedGroupsByJobId(jobId) {
+ return this.importStates[jobId]?.groups ?? [];
+ }
+
getImportStateFromStorageByGroupId(groupId) {
- const PREFIX = this.getStorageKey('');
const [jobId, importState] =
- Object.entries(this.importStates).find(
- ([key, state]) => key.startsWith(PREFIX) && state.groups.some((g) => g.id === groupId),
- ) ?? [];
+ Object.entries(this.importStates)
+ .reverse()
+ .find(([, state]) => state.groups.some((g) => g.id === groupId)) ?? [];
if (!jobId) {
return null;
@@ -67,10 +73,6 @@ export class SourceGroupsManager {
return { jobId, importState: { ...group, status: importState.status } };
}
- getStorageKey(importId) {
- return `${this.sourceUrl}|${importId}`;
- }
-
saveImportStatesToStorage = debounce(() => {
try {
// storage might be changed in other tab so fetch first
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
index c830aaa75e6..6ef4bbafec0 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -30,6 +30,7 @@ type ClientBulkImportSourceGroup {
full_name: String!
progress: ClientBulkImportProgress!
import_target: ClientBulkImportTarget!
+ last_import_target: ClientBulkImportTarget
validation_errors: [ClientBulkImportValidationError!]!
}
@@ -50,11 +51,21 @@ extend type Query {
availableNamespaces: [ClientBulkImportAvailableNamespace!]!
}
+input InputTargetInput {
+ target_namespace: String!
+ new_name: String!
+}
+
extend type Mutation {
setNewName(newName: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
setTargetNamespace(targetNamespace: String, sourceGroupId: ID!): ClientBulkImportSourceGroup!
importGroups(sourceGroupIds: [ID!]!): [ClientBulkImportSourceGroup!]!
- setImportProgress(id: ID, status: String!): ClientBulkImportSourceGroup!
+ setImportProgress(
+ id: ID
+ status: String!
+ jobId: String
+ importTarget: ImportTargetInput!
+ ): ClientBulkImportSourceGroup!
updateImportProgress(id: ID, status: String!): ClientBulkImportProgress
addValidationError(
sourceGroupId: ID!
diff --git a/app/assets/javascripts/import_entities/import_groups/utils.js b/app/assets/javascripts/import_entities/import_groups/utils.js
index b451008b6f9..a1baeaf39dd 100644
--- a/app/assets/javascripts/import_entities/import_groups/utils.js
+++ b/app/assets/javascripts/import_entities/import_groups/utils.js
@@ -1,3 +1,4 @@
+import { STATUSES } from '../constants';
import { NEW_NAME_FIELD } from './constants';
export function isNameValid(group, validationRegex) {
@@ -11,3 +12,11 @@ export function getInvalidNameValidationMessage(group) {
export function isInvalid(group, validationRegex) {
return Boolean(!isNameValid(group, validationRegex) || getInvalidNameValidationMessage(group));
}
+
+export function isFinished(group) {
+ return group.progress.status === STATUSES.FINISHED;
+}
+
+export function isAvailableForImport(group) {
+ return [STATUSES.NONE, STATUSES.FINISHED].some((status) => group.progress.status === status);
+}
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 14d08caef34..0cd3519bcec 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
@@ -32,7 +32,7 @@ export default {
},
computed: {
- ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace']),
+ ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
...mapGetters([
'isLoading',
'isImportingAnyRepo',
@@ -43,7 +43,7 @@ export default {
]),
pagePaginationStateKey() {
- return `${this.filter}-${this.repositories.length}`;
+ return `${this.filter}-${this.repositories.length}-${this.pageInfo.page}`;
},
availableNamespaces() {
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 5cbc6e85bf3..92be028b8a9 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -53,7 +53,6 @@ const importAll = ({ state, dispatch }) => {
const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
const nextPage = state.pageInfo.page + 1;
- commit(types.SET_PAGE, nextPage);
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@@ -67,11 +66,10 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
}),
)
.then(({ data }) => {
+ commit(types.SET_PAGE, nextPage);
commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
})
.catch((e) => {
- commit(types.SET_PAGE, nextPage - 1);
-
if (hasRedirectInError(e)) {
redirectToUrlInError(e);
} else if (tooManyRequests(e)) {
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
index c5e1922597a..45f7a684161 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -9,7 +9,7 @@ const makeNewImportedProject = (importedProject) => ({
sanitizedName: importedProject.name,
providerLink: importedProject.providerLink,
},
- importedProject,
+ importedProject: { ...importedProject },
});
const makeNewIncompatibleProject = (project) => ({
@@ -63,15 +63,16 @@ export default {
factory: makeNewIncompatibleProject,
});
- state.repositories = [
- ...newImportedProjects,
- ...state.repositories,
- ...repositories.providerRepos.map((project) => ({
+ const existingProjects = [...newImportedProjects, ...state.repositories];
+ const existingProjectNames = new Set(existingProjects.map((p) => p.importSource.fullName));
+ const newProjects = repositories.providerRepos
+ .filter((project) => !existingProjectNames.has(project.fullName))
+ .map((project) => ({
importSource: project,
importedProject: null,
- })),
- ...newIncompatibleProjects,
- ];
+ }));
+
+ state.repositories = [...existingProjects, ...newProjects, ...newIncompatibleProjects];
if (incompatibleRepos.length === 0 && repositories.providerRepos.length === 0) {
state.pageInfo.page -= 1;
diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js
index 8644ff3a249..6e6461cd7a9 100644
--- a/app/assets/javascripts/incidents/list.js
+++ b/app/assets/javascripts/incidents/list.js
@@ -24,7 +24,7 @@ export default () => {
} = domEl.dataset;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
return new Vue({
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
deleted file mode 100644
index b42264c870b..00000000000
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { stickyMonitor } from './lib/utils/sticky';
-
-export default (stickyTop) => {
- stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
-
- initDeprecatedJQueryDropdown($('.js-diff-stats-dropdown'), {
- filterable: true,
- remoteFilter: false,
- });
-};
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_deprecated_notes.js
index a77828e8cf2..5f918b0d2f5 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_deprecated_notes.js
@@ -1,4 +1,4 @@
-import Notes from './notes';
+import Notes from './deprecated_notes';
export default () => {
const dataEl = document.querySelector('.js-notes-data');
diff --git a/app/assets/javascripts/init_diff_stats_dropdown.js b/app/assets/javascripts/init_diff_stats_dropdown.js
new file mode 100644
index 00000000000..27df761a103
--- /dev/null
+++ b/app/assets/javascripts/init_diff_stats_dropdown.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+import DiffStatsDropdown from '~/vue_shared/components/diff_stats_dropdown.vue';
+import { stickyMonitor } from './lib/utils/sticky';
+
+export const initDiffStatsDropdown = (stickyTop) => {
+ if (stickyTop) {
+ stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
+ }
+
+ const el = document.querySelector('.js-diff-stats-dropdown');
+
+ if (!el) {
+ return false;
+ }
+
+ const { changed, added, deleted, files } = el.dataset;
+
+ return new Vue({
+ el,
+ render: (createElement) =>
+ createElement(DiffStatsDropdown, {
+ props: {
+ changed: parseInt(changed, 10),
+ added: parseInt(added, 10),
+ deleted: parseInt(deleted, 10),
+ files: JSON.parse(files),
+ },
+ }),
+ });
+};
diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js
index 17c73fdf1c3..7a70d893008 100644
--- a/app/assets/javascripts/init_issuable_sidebar.js
+++ b/app/assets/javascripts/init_issuable_sidebar.js
@@ -1,9 +1,7 @@
/* eslint-disable no-new */
-import { mountSidebarLabels, getSidebarOptions } from '~/sidebar/mount_sidebar';
+import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import IssuableContext from './issuable_context';
-import LabelsSelect from './labels_select';
-import MilestoneSelect from './milestone_select';
import Sidebar from './right_sidebar';
export default () => {
@@ -13,12 +11,6 @@ export default () => {
const sidebarOptions = getSidebarOptions(sidebarOptEl);
- new MilestoneSelect({
- full_path: sidebarOptions.fullPath,
- });
- new LabelsSelect();
new IssuableContext(sidebarOptions.currentUser);
Sidebar.initialize();
-
- mountSidebarLabels();
};
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index 11e9b25f9a3..1cc5a185f03 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -37,7 +37,7 @@ const issueTransitionOptions = [
help: s__(
'JiraService|Automatically transitions Jira issues to the "Done" category. %{linkStart}Learn more%{linkEnd}',
),
- link: helpPagePath('integration/jira/index.html', {
+ link: helpPagePath('integration/jira/issues.html', {
anchor: 'automatic-issue-transitions',
}),
},
@@ -47,7 +47,7 @@ const issueTransitionOptions = [
help: s__(
'JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}',
),
- link: helpPagePath('integration/jira/index.html', {
+ link: helpPagePath('integration/jira/issues.html', {
anchor: 'custom-issue-transitions',
}),
},
diff --git a/app/assets/javascripts/invite_members/components/import_a_project_modal.vue b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue
new file mode 100644
index 00000000000..d71468284ca
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/import_a_project_modal.vue
@@ -0,0 +1,157 @@
+<script>
+import { GlButton, GlFormGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import { importProjectMembers } from '~/api/projects_api';
+import { s__, __, sprintf } from '~/locale';
+import ProjectSelect from './project_select.vue';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlModal,
+ GlSprintf,
+ ProjectSelect,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ projectName: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ projectToBeImported: {},
+ invalidFeedbackMessage: '',
+ isLoading: false,
+ };
+ },
+ computed: {
+ modalIntro() {
+ return sprintf(this.$options.i18n.modalIntro, {
+ name: this.projectName,
+ });
+ },
+ importDisabled() {
+ return Object.keys(this.projectToBeImported).length === 0;
+ },
+ validationState() {
+ return this.invalidFeedbackMessage === '' ? null : false;
+ },
+ },
+ methods: {
+ submitImport() {
+ this.isLoading = true;
+ return importProjectMembers(this.projectId, this.projectToBeImported.id)
+ .then(this.showToastMessage)
+ .catch(this.showErrorAlert)
+ .finally(() => {
+ this.isLoading = false;
+ this.projectToBeImported = {};
+ });
+ },
+ closeModal() {
+ this.invalidFeedbackMessage = '';
+
+ this.$refs.modal.hide();
+ },
+ showToastMessage() {
+ this.$toast.show(this.$options.i18n.successMessage, this.$options.toastOptions);
+
+ this.closeModal();
+ },
+ showErrorAlert() {
+ this.invalidFeedbackMessage = this.$options.i18n.defaultError;
+ },
+ },
+ toastOptions() {
+ return {
+ onComplete: () => {
+ this.projectToBeImported = {};
+ },
+ };
+ },
+ i18n: {
+ buttonText: s__('ImportAProjectModal|Import from a project'),
+ projectLabel: __('Project'),
+ modalTitle: s__('ImportAProjectModal|Import members from another project'),
+ modalIntro: s__(
+ "ImportAProjectModal|You're importing members to the %{strongStart}%{name}%{strongEnd} project.",
+ ),
+ modalHelpText: s__(
+ 'ImportAProjectModal|Only project members (not group members) are imported, and they get the same permissions as the project you import from.',
+ ),
+ modalPrimaryButton: s__('ImportAProjectModal|Import project members'),
+ modalCancelButton: __('Cancel'),
+ defaultError: s__('ImportAProjectModal|Unable to import project members'),
+ successMessage: s__('ImportAProjectModal|Successfully imported'),
+ },
+ projectSelectLabelId: 'project-select',
+ modalId: uniqueId('import-a-project-modal-'),
+ formClasses: 'gl-mt-3 gl-sm-w-auto gl-w-full',
+ buttonClasses: 'gl-w-full',
+};
+</script>
+
+<template>
+ <form :class="$options.formClasses">
+ <gl-button v-gl-modal="$options.modalId" :class="$options.buttonClasses" variant="default">{{
+ $options.i18n.buttonText
+ }}</gl-button>
+
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modalId"
+ size="sm"
+ :title="$options.i18n.modalTitle"
+ ok-variant="danger"
+ footer-class="gl-bg-gray-10 gl-p-5"
+ >
+ <div>
+ <p ref="modalIntro">
+ <gl-sprintf :message="modalIntro">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <gl-form-group
+ :invalid-feedback="invalidFeedbackMessage"
+ :state="validationState"
+ data-testid="form-group"
+ >
+ <label :id="$options.projectSelectLabelId" class="col-form-label">{{
+ $options.i18n.projectLabel
+ }}</label>
+ <project-select v-model="projectToBeImported" />
+ </gl-form-group>
+ <p>{{ $options.i18n.modalHelpText }}</p>
+ </div>
+ <template #modal-footer>
+ <div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0"
+ >
+ <gl-button data-testid="cancel-button" @click="closeModal">
+ {{ $options.i18n.modalCancelButton }}
+ </gl-button>
+ <div class="gl-mr-3"></div>
+ <gl-button
+ :disabled="importDisabled"
+ :loading="isLoading"
+ variant="success"
+ data-testid="import-button"
+ @click="submitImport"
+ >{{ $options.i18n.modalPrimaryButton }}</gl-button
+ >
+ </div>
+ </template>
+ </gl-modal>
+ </form>
+</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index ec7d466336e..05be427742c 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -65,7 +65,7 @@ export default {
if (this.event && this.label) {
return {
...baseAttributes,
- 'data-track-event': this.event,
+ 'data-track-action': this.event,
'data-track-label': this.label,
};
}
diff --git a/app/assets/javascripts/invite_members/components/project_select.vue b/app/assets/javascripts/invite_members/components/project_select.vue
new file mode 100644
index 00000000000..b7a3918813b
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/project_select.vue
@@ -0,0 +1,143 @@
+<script>
+import {
+ GlAvatarLabeled,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
+import { getProjects } from '~/rest_api';
+import { SEARCH_DELAY, GROUP_FILTERS } from '../constants';
+
+export default {
+ name: 'ProjectSelect',
+ components: {
+ GlAvatarLabeled,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ },
+ model: {
+ prop: 'selectedProject',
+ },
+ props: {
+ groupsFilter: {
+ type: String,
+ required: false,
+ default: GROUP_FILTERS.ALL,
+ validator: (value) => Object.values(GROUP_FILTERS).includes(value),
+ },
+ parentGroupId: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ data() {
+ return {
+ isFetching: false,
+ projects: [],
+ selectedProject: {},
+ searchTerm: '',
+ errorMessage: '',
+ };
+ },
+ computed: {
+ selectedProjectName() {
+ return this.selectedProject.name || this.$options.i18n.dropdownText;
+ },
+ isFetchResultEmpty() {
+ return this.projects.length === 0 && !this.isFetching;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.retrieveProjects();
+ },
+ },
+ mounted() {
+ this.retrieveProjects();
+ },
+ methods: {
+ retrieveProjects: debounce(function debouncedRetrieveProjects() {
+ this.isFetching = true;
+ this.errorMessage = '';
+ return this.fetchProjects()
+ .then((response) => {
+ this.projects = response.data.map((project) => ({
+ ...convertObjectPropsToCamelCase(project),
+ name: project.name_with_namespace,
+ }));
+ })
+ .catch(() => {
+ this.errorMessage = this.$options.i18n.errorFetchingProjects;
+ })
+ .finally(() => {
+ this.isFetching = false;
+ });
+ }, SEARCH_DELAY),
+ fetchProjects() {
+ return getProjects(this.searchTerm, this.$options.defaultFetchOptions);
+ },
+ selectProject(project) {
+ this.selectedProject = project;
+
+ this.$emit('input', this.selectedProject);
+ },
+ },
+ i18n: {
+ dropdownText: s__('ProjectSelect|Select a project'),
+ searchPlaceholder: s__('ProjectSelect|Search projects'),
+ emptySearchResult: s__('ProjectSelect|No matching results'),
+ errorFetchingProjects: s__(
+ 'ProjectSelect|There was an error fetching the projects. Please try again.',
+ ),
+ },
+ defaultFetchOptions: {
+ exclude_internal: true,
+ active: true,
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-dropdown
+ data-testid="project-select-dropdown"
+ :text="selectedProjectName"
+ toggle-class="gl-mb-2"
+ block
+ menu-class="gl-w-full!"
+ >
+ <gl-search-box-by-type
+ v-model="searchTerm"
+ :is-loading="isFetching"
+ :placeholder="$options.i18n.searchPlaceholder"
+ data-qa-selector="project_select_dropdown_search_field"
+ />
+ <gl-dropdown-item
+ v-for="project in projects"
+ :key="project.id"
+ :name="project.name"
+ @click="selectProject(project)"
+ >
+ <gl-avatar-labeled
+ :label="project.name"
+ :src="project.avatarUrl"
+ :entity-id="project.id"
+ :entity-name="project.name"
+ :size="32"
+ />
+ </gl-dropdown-item>
+ <gl-dropdown-text v-if="errorMessage" data-testid="error-message">
+ <span class="gl-text-gray-500">{{ errorMessage }}</span>
+ </gl-dropdown-text>
+ <gl-dropdown-text v-else-if="isFetchResultEmpty" data-testid="empty-result-message">
+ <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
+ </gl-dropdown-text>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/invite_members/init_import_a_project_modal.js b/app/assets/javascripts/invite_members/init_import_a_project_modal.js
new file mode 100644
index 00000000000..954347467de
--- /dev/null
+++ b/app/assets/javascripts/invite_members/init_import_a_project_modal.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import ImportAProjectModal from '~/invite_members/components/import_a_project_modal.vue';
+
+export default function initImportAProjectModal() {
+ const el = document.querySelector('.js-import-a-project-modal');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectId, projectName } = el.dataset;
+
+ return new Vue({
+ el,
+ render: (createElement) =>
+ createElement(ImportAProjectModal, {
+ props: {
+ projectId,
+ projectName,
+ },
+ }),
+ });
+}
diff --git a/app/assets/javascripts/issuable/components/csv_export_modal.vue b/app/assets/javascripts/issuable/components/csv_export_modal.vue
index 5c880cbfad8..1c88f8dfdca 100644
--- a/app/assets/javascripts/issuable/components/csv_export_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_export_modal.vue
@@ -63,7 +63,7 @@ export default {
</gl-sprintf>
<gl-sprintf
v-else
- :message="n__('1 merge request selected', '%d merge request selected', issuableCount)"
+ :message="n__('1 merge request selected', '%d merge requests selected', issuableCount)"
>
<template #issuableCount>{{ issuableCount }}</template>
</gl-sprintf>
@@ -89,7 +89,7 @@ export default {
:href="exportCsvPath"
data-method="post"
:data-qa-selector="`export_${issuableType}_button`"
- data-track-event="click_button"
+ data-track-action="click_button"
:data-track-label="`export_${issuableType}_csv`"
>
<gl-sprintf :message="__('Export %{name}')">
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue
index 29dd0b7fed5..df9d5c86a4b 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_item.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue
@@ -69,6 +69,9 @@ export default {
isIssuableUrlExternal() {
return isExternal(this.webUrl);
},
+ reference() {
+ return this.issuable.reference || `${this.issuableSymbol}${this.issuable.iid}`;
+ },
labels() {
return this.issuable.labels?.nodes || this.issuable.labels || [];
},
@@ -201,9 +204,9 @@ export default {
</div>
<div class="issuable-info">
<slot v-if="hasSlotContents('reference')" name="reference"></slot>
- <span v-else data-testid="issuable-reference" class="issuable-reference"
- >{{ issuableSymbol }}{{ issuable.iid }}</span
- >
+ <span v-else data-testid="issuable-reference" class="issuable-reference">
+ {{ reference }}
+ </span>
<span class="issuable-authored gl-display-none gl-sm-display-inline-block! gl-mr-3">
<span aria-hidden="true">&middot;</span>
<span
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index b7e24a8b17e..2c9a512acdb 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
+import { GlIcon, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
@@ -32,6 +32,9 @@ export default {
formComponent,
PinnedLinks,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
endpoint: {
required: true,
@@ -183,6 +186,11 @@ export default {
required: false,
default: true,
},
+ isHidden: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const store = new Store({
@@ -508,6 +516,15 @@ export default {
<span v-if="isConfidential" data-testid="confidential" class="issuable-warning-icon">
<gl-icon name="eye-slash" :aria-label="__('Confidential')" />
</span>
+ <span
+ v-if="isHidden"
+ v-gl-tooltip
+ :title="__('This issue is hidden because its author has been banned')"
+ data-testid="hidden"
+ class="issuable-warning-icon"
+ >
+ <gl-icon name="spam" />
+ </span>
<p
class="gl-font-weight-bold gl-overflow-hidden gl-white-space-nowrap gl-text-overflow-ellipsis gl-my-0"
:title="state.titleText"
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index 0812392f804..4c6a1478e95 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -123,6 +123,7 @@ export default {
}
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
@@ -136,7 +137,7 @@ export default {
>
<div
ref="gfm-content"
- v-safe-html="descriptionHtml"
+ v-safe-html:[$options.safeHtmlConfig]="descriptionHtml"
:class="{
'issue-realtime-pre-pulse': preAnimation,
'issue-realtime-trigger-pulse': pulseAnimation,
diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue
index 96f5a7c88e0..f3c2a31bd5b 100644
--- a/app/assets/javascripts/issue_show/components/locked_warning.vue
+++ b/app/assets/javascripts/issue_show/components/locked_warning.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { __, sprintf } from '~/locale';
export default {
@@ -24,5 +23,8 @@ export default {
</script>
<template>
- <div class="alert alert-danger" v-html="alertMessage"></div>
+ <div
+ class="alert alert-danger"
+ v-html="alertMessage /* eslint-disable-line vue/no-v-html */"
+ ></div>
</template>
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
index 60b01a6d37f..6dc7460b037 100644
--- a/app/assets/javascripts/issues_list/components/issuable.vue
+++ b/app/assets/javascripts/issues_list/components/issuable.vue
@@ -315,7 +315,7 @@ export default {
<span
v-if="isJiraIssue"
v-safe-html="jiraLogo"
- class="svg-container jira-logo-container"
+ class="svg-container logo-container"
data-testid="jira-logo"
></span>
{{ referencePath }}
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 ee0429c0432..8e37339fca6 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -9,11 +9,12 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import { cloneDeep } from 'lodash';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
import createFlash from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { ITEM_TYPE } from '~/groups/constants';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
@@ -21,7 +22,6 @@ import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
CREATED_DESC,
i18n,
- issuesCountSmartQueryBase,
MAX_LIST_SIZE,
PAGE_SIZE,
PARAM_DUE_DATE,
@@ -117,9 +117,15 @@ export default {
exportCsvPath: {
default: '',
},
+ fullPath: {
+ default: '',
+ },
groupEpicsPath: {
default: '',
},
+ hasAnyIssues: {
+ default: false,
+ },
hasBlockedIssuesFeature: {
default: false,
},
@@ -132,17 +138,14 @@ export default {
hasMultipleIssueAssigneesFeature: {
default: false,
},
- hasProjectIssues: {
- default: false,
- },
initialEmail: {
default: '',
},
- isSignedIn: {
+ isProject: {
default: false,
},
- issuesPath: {
- default: '',
+ isSignedIn: {
+ default: false,
},
jiraIntegrationPath: {
default: '',
@@ -150,9 +153,6 @@ export default {
newIssuePath: {
default: '',
},
- projectPath: {
- default: '',
- },
rssPath: {
default: '',
},
@@ -164,18 +164,16 @@ export default {
},
},
data() {
- const filterTokens = getFilterTokens(window.location.search);
const state = getParameterByName(PARAM_STATE);
const sortKey = getSortKey(getParameterByName(PARAM_SORT));
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
- this.initialFilterTokens = cloneDeep(filterTokens);
-
return {
dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)),
exportCsvPathWithQuery: this.getExportCsvPathWithQuery(),
- filterTokens,
+ filterTokens: getFilterTokens(window.location.search),
issues: [],
+ issuesCounts: {},
pageInfo: {},
pageParams: getInitialPageParams(sortKey),
showBulkEditSidebar: false,
@@ -189,61 +187,47 @@ export default {
variables() {
return this.queryVariables;
},
- update: ({ project }) => project?.issues.nodes ?? [],
+ update(data) {
+ return data[this.namespace]?.issues.nodes ?? [];
+ },
result({ data }) {
- this.pageInfo = data.project?.issues.pageInfo ?? {};
+ this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error(error) {
createFlash({ message: this.$options.i18n.errorFetchingIssues, captureError: true, error });
},
skip() {
- return !this.hasProjectIssues;
+ return !this.hasAnyIssues;
},
debounce: 200,
},
- countOpened: {
- ...issuesCountSmartQueryBase,
+ issuesCounts: {
+ query: getIssuesCountsQuery,
variables() {
- return {
- ...this.queryVariables,
- state: IssuableStates.Opened,
- };
+ return this.queryVariables;
},
- skip() {
- return !this.hasProjectIssues;
+ update(data) {
+ return data[this.namespace] ?? {};
},
- },
- countClosed: {
- ...issuesCountSmartQueryBase,
- variables() {
- return {
- ...this.queryVariables,
- state: IssuableStates.Closed,
- };
+ error(error) {
+ createFlash({ message: this.$options.i18n.errorFetchingCounts, captureError: true, error });
},
skip() {
- return !this.hasProjectIssues;
+ return !this.hasAnyIssues;
},
- },
- countAll: {
- ...issuesCountSmartQueryBase,
- variables() {
- return {
- ...this.queryVariables,
- state: IssuableStates.All,
- };
- },
- skip() {
- return !this.hasProjectIssues;
+ debounce: 200,
+ context: {
+ isSingleRequest: true,
},
},
},
computed: {
queryVariables() {
return {
+ fullPath: this.fullPath,
+ isProject: this.isProject,
isSignedIn: this.isSignedIn,
- projectPath: this.projectPath,
search: this.searchQuery,
sort: this.sortKey,
state: this.state,
@@ -251,6 +235,9 @@ export default {
...this.apiFilterParams,
};
},
+ namespace() {
+ return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
+ },
hasSearch() {
return this.searchQuery || Object.keys(this.urlFilterParams).length;
},
@@ -263,6 +250,9 @@ export default {
isOpenTab() {
return this.state === IssuableStates.Opened;
},
+ showCsvButtons() {
+ return this.isProject && this.isSignedIn;
+ },
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
@@ -405,10 +395,11 @@ export default {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
},
tabCounts() {
+ const { openedIssues, closedIssues, allIssues } = this.issuesCounts;
return {
- [IssuableStates.Opened]: this.countOpened,
- [IssuableStates.Closed]: this.countClosed,
- [IssuableStates.All]: this.countAll,
+ [IssuableStates.Opened]: openedIssues?.count,
+ [IssuableStates.Closed]: closedIssues?.count,
+ [IssuableStates.All]: allIssues?.count,
};
},
currentTabCount() {
@@ -465,39 +456,41 @@ export default {
return this.$apollo
.query({
query: searchLabelsQuery,
- variables: { projectPath: this.projectPath, search },
+ variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
- .then(({ data }) => data.project.labels.nodes);
+ .then(({ data }) => data[this.namespace]?.labels.nodes);
},
fetchMilestones(search) {
return this.$apollo
.query({
query: searchMilestonesQuery,
- variables: { projectPath: this.projectPath, search },
+ variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
- .then(({ data }) => data.project.milestones.nodes);
+ .then(({ data }) => data[this.namespace]?.milestones.nodes);
},
fetchIterations(search) {
const id = Number(search);
const variables =
!search || Number.isNaN(id)
- ? { projectPath: this.projectPath, search }
- : { projectPath: this.projectPath, id };
+ ? { fullPath: this.fullPath, search, isProject: this.isProject }
+ : { fullPath: this.fullPath, id, isProject: this.isProject };
return this.$apollo
.query({
query: searchIterationsQuery,
variables,
})
- .then(({ data }) => data.project.iterations.nodes);
+ .then(({ data }) => data[this.namespace]?.iterations.nodes);
},
fetchUsers(search) {
return this.$apollo
.query({
query: searchUsersQuery,
- variables: { projectPath: this.projectPath, search },
+ variables: { fullPath: this.fullPath, search, isProject: this.isProject },
})
- .then(({ data }) => data.project.projectMembers.nodes.map((member) => member.user));
+ .then(({ data }) =>
+ data[this.namespace]?.[`${this.namespace}Members`].nodes.map((member) => member.user),
+ );
},
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
@@ -578,19 +571,20 @@ export default {
}
return axios
- .put(joinPaths(this.issuesPath, issueToMove.iid, 'reorder'), {
+ .put(joinPaths(issueToMove.webPath, 'reorder'), {
move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId),
move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId),
+ group_full_path: this.isProject ? undefined : this.fullPath,
})
.then(() => {
const serializedVariables = JSON.stringify(this.queryVariables);
- this.$apollo.mutate({
+ return this.$apollo.mutate({
mutation: reorderIssuesMutation,
- variables: { oldIndex, newIndex, serializedVariables },
+ variables: { oldIndex, newIndex, namespace: this.namespace, serializedVariables },
});
})
- .catch(() => {
- createFlash({ message: this.$options.i18n.reorderError });
+ .catch((error) => {
+ createFlash({ message: this.$options.i18n.reorderError, captureError: true, error });
});
},
handleSort(sortKey) {
@@ -607,13 +601,13 @@ export default {
</script>
<template>
- <div v-if="hasProjectIssues">
+ <div v-if="hasAnyIssues">
<issuable-list
- :namespace="projectPath"
+ :namespace="fullPath"
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchPlaceholder"
:search-tokens="searchTokens"
- :initial-filter-value="initialFilterTokens"
+ :initial-filter-value="filterTokens"
:sort-options="sortOptions"
:initial-sort-by="sortKey"
:issuables="issues"
@@ -653,7 +647,7 @@ export default {
:aria-label="$options.i18n.calendarLabel"
/>
<csv-import-export-buttons
- v-if="isSignedIn"
+ v-if="showCsvButtons"
class="gl-md-mr-3"
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
@@ -766,6 +760,7 @@ export default {
{{ $options.i18n.newIssueLabel }}
</gl-button>
<csv-import-export-buttons
+ v-if="showCsvButtons"
class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery"
:issuable-count="currentTabCount"
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 3f5b0d1feb5..5bdc1bd9f90 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -1,5 +1,3 @@
-import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
-import createFlash from '~/flash';
import { __, s__ } from '~/locale';
import {
FILTER_ANY,
@@ -22,6 +20,7 @@ const MILESTONE_DUE = 'milestone_due';
const POPULARITY = 'popularity';
const WEIGHT = 'weight';
const LABEL_PRIORITY = 'label_priority';
+const TITLE = 'title';
export const RELATIVE_POSITION = 'relative_position';
export const LOADING_LIST_ITEMS_LENGTH = 8;
export const PAGE_SIZE = 20;
@@ -43,6 +42,8 @@ export const sortOrderMap = {
relative_position: { order_by: RELATIVE_POSITION, sort: ASC },
weight_desc: { order_by: WEIGHT, sort: DESC },
weight: { order_by: WEIGHT, sort: ASC },
+ title: { order_by: TITLE, sort: ASC },
+ title_desc: { order_by: TITLE, sort: DESC },
};
export const availableSortOptionsJira = [
@@ -146,6 +147,8 @@ export const POPULARITY_DESC = 'POPULARITY_DESC';
export const PRIORITY_ASC = 'PRIORITY_ASC';
export const PRIORITY_DESC = 'PRIORITY_DESC';
export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
+export const TITLE_ASC = 'TITLE_ASC';
+export const TITLE_DESC = 'TITLE_DESC';
export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
@@ -163,6 +166,7 @@ const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
+const TITLE_DESC_SORT = 'title_desc';
export const urlSortParams = {
[PRIORITY_ASC]: PRIORITY_ASC_SORT,
@@ -183,6 +187,8 @@ export const urlSortParams = {
[WEIGHT_ASC]: WEIGHT,
[WEIGHT_DESC]: WEIGHT_DESC_SORT,
[BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
+ [TITLE_ASC]: TITLE,
+ [TITLE_DESC]: TITLE_DESC_SORT,
};
export const MAX_LIST_SIZE = 10;
@@ -351,15 +357,3 @@ export const filters = {
},
},
};
-
-export const issuesCountSmartQueryBase = {
- query: getIssuesCountQuery,
- context: {
- isSingleRequest: true,
- },
- update: ({ project }) => project?.issues.count,
- error(error) {
- createFlash({ message: i18n.errorFetchingCounts, captureError: true, error });
- },
- debounce: 200,
-};
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index dcc7ee72273..e89e3e8e681 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -85,17 +85,17 @@ export function mountIssuesListApp() {
const resolvers = {
Mutation: {
- reorderIssues: (_, { oldIndex, newIndex, serializedVariables }, { cache }) => {
+ reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => {
const variables = JSON.parse(serializedVariables);
const sourceData = cache.readQuery({ query: getIssuesQuery, variables });
const data = produce(sourceData, (draftData) => {
- const issues = draftData.project.issues.nodes.slice();
+ const issues = draftData[namespace].issues.nodes.slice();
const issueToMove = issues[oldIndex];
issues.splice(oldIndex, 1);
issues.splice(newIndex, 0, issueToMove);
- draftData.project.issues.nodes = issues;
+ draftData[namespace].issues.nodes = issues;
});
cache.writeQuery({ query: getIssuesQuery, variables, data });
@@ -118,23 +118,23 @@ export function mountIssuesListApp() {
emailsHelpPagePath,
emptyStateSvgPath,
exportCsvPath,
+ fullPath,
groupEpicsPath,
+ hasAnyIssues,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasIterationsFeature,
hasMultipleIssueAssigneesFeature,
- hasProjectIssues,
importCsvIssuesPath,
initialEmail,
+ isProject,
isSignedIn,
- issuesPath,
jiraIntegrationPath,
markdownHelpPath,
maxAttachmentSize,
newIssuePath,
projectImportJiraPath,
- projectPath,
quickActionsHelpPath,
resetPath,
rssPath,
@@ -150,18 +150,18 @@ export function mountIssuesListApp() {
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
+ fullPath,
groupEpicsPath,
+ hasAnyIssues: parseBoolean(hasAnyIssues),
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
- hasProjectIssues: parseBoolean(hasProjectIssues),
+ isProject: parseBoolean(isProject),
isSignedIn: parseBoolean(isSignedIn),
- issuesPath,
jiraIntegrationPath,
newIssuePath,
- projectPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
signInPath,
@@ -172,9 +172,9 @@ export function mountIssuesListApp() {
importCsvIssuesPath,
maxAttachmentSize,
projectImportJiraPath,
- showExportButton: parseBoolean(hasProjectIssues),
+ showExportButton: parseBoolean(hasAnyIssues),
showImportButton: parseBoolean(canImportIssues),
- showLabel: !parseBoolean(hasProjectIssues),
+ showLabel: !parseBoolean(hasAnyIssues),
// For IssuableByEmail component
emailsHelpPagePath,
initialEmail,
diff --git a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
index 30a01b4c3b0..6df72cf6596 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -1,9 +1,10 @@
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "./issue.fragment.graphql"
-query getProjectIssues(
+query getIssues(
+ $isProject: Boolean = false
$isSignedIn: Boolean = false
- $projectPath: ID!
+ $fullPath: ID!
$search: String
$sort: IssueSort
$state: IssuableState
@@ -20,7 +21,35 @@ query getProjectIssues(
$firstPageSize: Int
$lastPageSize: Int
) {
- project(fullPath: $projectPath) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ issues(
+ includeSubgroups: true
+ search: $search
+ sort: $sort
+ state: $state
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ before: $beforeCursor
+ after: $afterCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ ...IssueFragment
+ reference(full: true)
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
issues(
search: $search
sort: $sort
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
deleted file mode 100644
index e6896131da9..00000000000
--- a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
+++ /dev/null
@@ -1,30 +0,0 @@
-query getProjectIssuesCount(
- $projectPath: ID!
- $search: String
- $state: IssuableState
- $assigneeId: String
- $assigneeUsernames: [String!]
- $authorUsername: String
- $labelName: [String]
- $milestoneTitle: [String]
- $milestoneWildcardId: MilestoneWildcardId
- $types: [IssueType!]
- $not: NegatedIssueFilterInput
-) {
- project(fullPath: $projectPath) {
- issues(
- search: $search
- state: $state
- assigneeId: $assigneeId
- assigneeUsernames: $assigneeUsernames
- authorUsername: $authorUsername
- labelName: $labelName
- milestoneTitle: $milestoneTitle
- milestoneWildcardId: $milestoneWildcardId
- types: $types
- not: $not
- ) {
- count
- }
- }
-}
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
new file mode 100644
index 00000000000..7bcdbbb28fc
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
@@ -0,0 +1,105 @@
+query getIssuesCount(
+ $isProject: Boolean = false
+ $fullPath: ID!
+ $search: String
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $types: [IssueType!]
+ $not: NegatedIssueFilterInput
+) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ openedIssues: issues(
+ includeSubgroups: true
+ state: opened
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ closedIssues: issues(
+ includeSubgroups: true
+ state: closed
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ allIssues: issues(
+ includeSubgroups: true
+ state: all
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ openedIssues: issues(
+ state: opened
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ closedIssues: issues(
+ state: closed
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ allIssues: issues(
+ state: all
+ search: $search
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
index 633b06eced8..9c46cb3ef64 100644
--- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
@@ -13,6 +13,7 @@ fragment IssueFragment on Issue {
updatedAt
upvotes
userDiscussionsCount @include(if: $isSignedIn)
+ webPath
webUrl
assignees {
nodes {
diff --git a/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
new file mode 100644
index 00000000000..78a368089a8
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/iteration.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Iteration on Iteration {
+ id
+ title
+}
diff --git a/app/assets/javascripts/issues_list/queries/label.fragment.graphql b/app/assets/javascripts/issues_list/queries/label.fragment.graphql
new file mode 100644
index 00000000000..bb1d8f1ac9b
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/label.fragment.graphql
@@ -0,0 +1,6 @@
+fragment Label on Label {
+ id
+ color
+ textColor
+ title
+}
diff --git a/app/assets/javascripts/issues_list/queries/milestone.fragment.graphql b/app/assets/javascripts/issues_list/queries/milestone.fragment.graphql
new file mode 100644
index 00000000000..3cdf69bf585
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/milestone.fragment.graphql
@@ -0,0 +1,4 @@
+fragment Milestone on Milestone {
+ id
+ title
+}
diff --git a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
index 5927e3e83c7..160026a4742 100644
--- a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
+++ b/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
@@ -1,7 +1,13 @@
-mutation reorderIssues($oldIndex: Int, $newIndex: Int, $serializedVariables: String) {
+mutation reorderIssues(
+ $oldIndex: Int
+ $newIndex: Int
+ $namespace: String
+ $serializedVariables: String
+) {
reorderIssues(
oldIndex: $oldIndex
newIndex: $newIndex
+ namespace: $namespace
serializedVariables: $serializedVariables
) @client
}
diff --git a/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
index 11d9dcea573..93600c62905 100644
--- a/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
@@ -1,9 +1,17 @@
-query searchIterations($projectPath: ID!, $search: String, $id: ID) {
- project(fullPath: $projectPath) {
- iterations(title: $search, id: $id) {
+#import "./iteration.fragment.graphql"
+
+query searchIterations($fullPath: ID!, $search: String, $id: ID, $isProject: Boolean = false) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ iterations(title: $search, id: $id, includeAncestors: true) {
nodes {
- id
- title
+ ...Iteration
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
+ iterations(title: $search, id: $id, includeAncestors: true) {
+ nodes {
+ ...Iteration
}
}
}
diff --git a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
index de884e1221c..1515bd91da3 100644
--- a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
@@ -1,11 +1,17 @@
-query searchLabels($projectPath: ID!, $search: String) {
- project(fullPath: $projectPath) {
+#import "./label.fragment.graphql"
+
+query searchLabels($fullPath: ID!, $search: String, $isProject: Boolean = false) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ labels(searchTerm: $search, includeAncestorGroups: true, includeDescendantGroups: true) {
+ nodes {
+ ...Label
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
labels(searchTerm: $search, includeAncestorGroups: true) {
nodes {
- id
- color
- textColor
- title
+ ...Label
}
}
}
diff --git a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
index 91f74fd220b..8c6c50e9dc2 100644
--- a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
@@ -1,9 +1,17 @@
-query searchMilestones($projectPath: ID!, $search: String) {
- project(fullPath: $projectPath) {
+#import "./milestone.fragment.graphql"
+
+query searchMilestones($fullPath: ID!, $search: String, $isProject: Boolean = false) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ milestones(searchTitle: $search, includeAncestors: true, includeDescendants: true) {
+ nodes {
+ ...Milestone
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
milestones(searchTitle: $search, includeAncestors: true) {
nodes {
- id
- title
+ ...Milestone
}
}
}
diff --git a/app/assets/javascripts/issues_list/queries/search_users.query.graphql b/app/assets/javascripts/issues_list/queries/search_users.query.graphql
index 953157cfe3a..0211fc66235 100644
--- a/app/assets/javascripts/issues_list/queries/search_users.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/search_users.query.graphql
@@ -1,12 +1,20 @@
-query searchUsers($projectPath: ID!, $search: String) {
- project(fullPath: $projectPath) {
+#import "./user.fragment.graphql"
+
+query searchUsers($fullPath: ID!, $search: String, $isProject: Boolean = false) {
+ group(fullPath: $fullPath) @skip(if: $isProject) {
+ groupMembers(search: $search) {
+ nodes {
+ user {
+ ...User
+ }
+ }
+ }
+ }
+ project(fullPath: $fullPath) @include(if: $isProject) {
projectMembers(search: $search) {
nodes {
user {
- id
- avatarUrl
- name
- username
+ ...User
}
}
}
diff --git a/app/assets/javascripts/issues_list/queries/user.fragment.graphql b/app/assets/javascripts/issues_list/queries/user.fragment.graphql
new file mode 100644
index 00000000000..3e5bc0f7b93
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/user.fragment.graphql
@@ -0,0 +1,6 @@
+fragment User on User {
+ id
+ avatarUrl
+ name
+ username
+}
diff --git a/app/assets/javascripts/jira_connect/subscriptions/utils.js b/app/assets/javascripts/jira_connect/subscriptions/utils.js
index ecd1a31339a..ed7a9484a81 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/utils.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/utils.js
@@ -7,7 +7,7 @@ const isFunction = (fn) => typeof fn === 'function';
* Persist alert data to localStorage.
*/
export const persistAlert = ({ title, message, linkUrl, variant } = {}) => {
- if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (!AccessorUtilities.canUseLocalStorage()) {
return;
}
@@ -19,7 +19,7 @@ export const persistAlert = ({ title, message, linkUrl, variant } = {}) => {
* Return alert data from localStorage.
*/
export const retrieveAlert = () => {
- if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (!AccessorUtilities.canUseLocalStorage()) {
return null;
}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index fa9ee56c049..059772e8cb9 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -5,7 +5,7 @@ import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
-import { sprintf } from '~/locale';
+import { __, sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import delayedJobMixin from '../mixins/delayed_job_mixin';
import EmptyState from './empty_state.vue';
@@ -126,6 +126,9 @@ export default {
shouldRenderCodeQualityWalkthrough() {
return this.job.status.group === 'failed-with-warnings';
},
+ itemName() {
+ return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
+ },
},
watch: {
// Once the job log is loaded,
@@ -205,12 +208,11 @@ export default {
<div class="build-header top-area">
<ci-header
:status="job.status"
- :item-id="job.id"
:time="headerTime"
:user="job.user"
:has-sidebar-button="true"
:should-render-triggered-label="shouldRenderTriggeredLabel"
- :item-name="__('Job')"
+ :item-name="itemName"
@clickedSidebarButton="toggleSidebar"
/>
</div>
diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
index 376482b0319..6b3a4424a5b 100644
--- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
+++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue
@@ -1,14 +1,195 @@
<script>
+import { GlButton, GlButtonGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import {
+ ACTIONS_DOWNLOAD_ARTIFACTS,
+ ACTIONS_START_NOW,
+ ACTIONS_UNSCHEDULE,
+ ACTIONS_PLAY,
+ ACTIONS_RETRY,
+ CANCEL,
+ GENERIC_ERROR,
+ JOB_SCHEDULED,
+ PLAY_JOB_CONFIRMATION_MESSAGE,
+ RUN_JOB_NOW_HEADER_TITLE,
+} from '../constants';
+import eventHub from '../event_hub';
+import cancelJobMutation from '../graphql/mutations/job_cancel.mutation.graphql';
+import playJobMutation from '../graphql/mutations/job_play.mutation.graphql';
+import retryJobMutation from '../graphql/mutations/job_retry.mutation.graphql';
+import unscheduleJobMutation from '../graphql/mutations/job_unschedule.mutation.graphql';
+
export default {
+ ACTIONS_DOWNLOAD_ARTIFACTS,
+ ACTIONS_START_NOW,
+ ACTIONS_UNSCHEDULE,
+ ACTIONS_PLAY,
+ ACTIONS_RETRY,
+ CANCEL,
+ GENERIC_ERROR,
+ PLAY_JOB_CONFIRMATION_MESSAGE,
+ RUN_JOB_NOW_HEADER_TITLE,
+ jobRetry: 'jobRetry',
+ jobCancel: 'jobCancel',
+ jobPlay: 'jobPlay',
+ jobUnschedule: 'jobUnschedule',
+ playJobModalId: 'play-job-modal',
+ components: {
+ GlButton,
+ GlButtonGroup,
+ GlCountdown,
+ GlModal,
+ GlSprintf,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: {
+ admin: {
+ default: false,
+ },
+ },
props: {
job: {
type: Object,
required: true,
},
},
+ computed: {
+ artifactDownloadPath() {
+ return this.job.artifacts?.nodes[0]?.downloadPath;
+ },
+ canReadJob() {
+ return this.job.userPermissions?.readBuild;
+ },
+ isActive() {
+ return this.job.active;
+ },
+ manualJobPlayable() {
+ return this.job.playable && !this.admin && this.job.manualJob;
+ },
+ isRetryable() {
+ return this.job.retryable;
+ },
+ isScheduled() {
+ return this.job.status === JOB_SCHEDULED;
+ },
+ scheduledAt() {
+ return this.job.scheduledAt;
+ },
+ currentJobActionPath() {
+ return this.job.detailedStatus?.action?.path;
+ },
+ currentJobMethod() {
+ return this.job.detailedStatus?.action?.method;
+ },
+ shouldDisplayArtifacts() {
+ return this.job.userPermissions?.readJobArtifacts && this.job.artifacts?.nodes.length > 0;
+ },
+ },
+ methods: {
+ async postJobAction(name, mutation) {
+ try {
+ const {
+ data: {
+ [name]: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation,
+ variables: { id: this.job.id },
+ });
+ if (errors.length > 0) {
+ this.reportFailure();
+ } else {
+ eventHub.$emit('jobActionPerformed');
+ }
+ } catch {
+ this.reportFailure();
+ }
+ },
+ reportFailure() {
+ const toastProps = {
+ text: this.$options.GENERIC_ERROR,
+ variant: 'danger',
+ };
+
+ this.$toast.show(toastProps.text, {
+ variant: toastProps.variant,
+ });
+ },
+ cancelJob() {
+ this.postJobAction(this.$options.jobCancel, cancelJobMutation);
+ },
+ retryJob() {
+ this.postJobAction(this.$options.jobRetry, retryJobMutation);
+ },
+ playJob() {
+ this.postJobAction(this.$options.jobPlay, playJobMutation);
+ },
+ unscheduleJob() {
+ this.postJobAction(this.$options.jobUnschedule, unscheduleJobMutation);
+ },
+ },
};
</script>
<template>
- <div></div>
+ <gl-button-group>
+ <template v-if="canReadJob">
+ <gl-button v-if="isActive" icon="cancel" :title="$options.CANCEL" @click="cancelJob()" />
+ <template v-else-if="isScheduled">
+ <gl-button icon="planning" disabled data-testid="countdown">
+ <gl-countdown :end-date-string="scheduledAt" />
+ </gl-button>
+ <gl-button
+ v-gl-modal-directive="$options.playJobModalId"
+ icon="play"
+ :title="$options.ACTIONS_START_NOW"
+ data-testid="play-scheduled"
+ />
+ <gl-modal
+ :modal-id="$options.playJobModalId"
+ :title="$options.RUN_JOB_NOW_HEADER_TITLE"
+ @primary="playJob()"
+ >
+ <gl-sprintf :message="$options.PLAY_JOB_CONFIRMATION_MESSAGE">
+ <template #job_name>{{ job.name }}</template>
+ </gl-sprintf>
+ </gl-modal>
+ <gl-button
+ icon="time-out"
+ :title="$options.ACTIONS_UNSCHEDULE"
+ data-testid="unschedule"
+ @click="unscheduleJob()"
+ />
+ </template>
+ <template v-else>
+ <!--Note: This is the manual job play button -->
+ <gl-button
+ v-if="manualJobPlayable"
+ icon="play"
+ :title="$options.ACTIONS_PLAY"
+ data-testid="play"
+ @click="playJob()"
+ />
+ <gl-button
+ v-else-if="isRetryable"
+ icon="repeat"
+ :title="$options.ACTIONS_RETRY"
+ :method="currentJobMethod"
+ data-testid="retry"
+ @click="retryJob()"
+ />
+ </template>
+ </template>
+ <gl-button
+ v-if="shouldDisplayArtifacts"
+ icon="download"
+ :title="$options.ACTIONS_DOWNLOAD_ARTIFACTS"
+ :href="artifactDownloadPath"
+ rel="nofollow"
+ download
+ data-testid="download-artifacts"
+ />
+ </gl-button-group>
</template>
diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js
index 7e973a34e5c..e5d1bc01cbf 100644
--- a/app/assets/javascripts/jobs/components/table/constants.js
+++ b/app/assets/javascripts/jobs/components/table/constants.js
@@ -1,3 +1,5 @@
+import { s__, __ } from '~/locale';
+
export const GRAPHQL_PAGE_SIZE = 30;
export const initialPaginationState = {
@@ -7,3 +9,24 @@ export const initialPaginationState = {
first: GRAPHQL_PAGE_SIZE,
last: null,
};
+
+/* Error constants */
+export const POST_FAILURE = 'post_failure';
+export const DEFAULT = 'default';
+
+/* Job Status Constants */
+export const JOB_SCHEDULED = 'SCHEDULED';
+
+/* i18n */
+export const ACTIONS_DOWNLOAD_ARTIFACTS = __('Download artifacts');
+export const ACTIONS_START_NOW = s__('DelayedJobs|Start now');
+export const ACTIONS_UNSCHEDULE = s__('DelayedJobs|Unschedule');
+export const ACTIONS_PLAY = __('Play');
+export const ACTIONS_RETRY = __('Retry');
+
+export const CANCEL = __('Cancel');
+export const GENERIC_ERROR = __('An error occurred while making the request.');
+export const PLAY_JOB_CONFIRMATION_MESSAGE = s__(
+ `DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after its timer finishes.`,
+);
+export const RUN_JOB_NOW_HEADER_TITLE = s__('DelayedJobs|Run the delayed job now?');
diff --git a/app/assets/javascripts/jobs/components/table/event_hub.js b/app/assets/javascripts/jobs/components/table/event_hub.js
new file mode 100644
index 00000000000..e31806ad199
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/event_hub.js
@@ -0,0 +1,3 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+export default createEventHub();
diff --git a/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql b/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
new file mode 100644
index 00000000000..06b065a86ce
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/graphql/fragments/job.fragment.graphql
@@ -0,0 +1,3 @@
+fragment Job on CiJob {
+ id
+}
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql
new file mode 100644
index 00000000000..20935514d51
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_cancel.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/job.fragment.graphql"
+
+mutation cancelJob($id: CiBuildID!) {
+ jobCancel(input: { id: $id }) {
+ job {
+ ...Job
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql
new file mode 100644
index 00000000000..c94b045ac40
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_play.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/job.fragment.graphql"
+
+mutation playJob($id: CiBuildID!) {
+ jobPlay(input: { id: $id }) {
+ job {
+ ...Job
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql
new file mode 100644
index 00000000000..6e51f9a20fa
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_retry.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/job.fragment.graphql"
+
+mutation retryJob($id: CiBuildID!) {
+ jobRetry(input: { id: $id }) {
+ job {
+ ...Job
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql
new file mode 100644
index 00000000000..8be8c42f3c3
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/table/graphql/mutations/job_unschedule.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/job.fragment.graphql"
+
+mutation unscheduleJob($id: CiBuildID!) {
+ jobUnschedule(input: { id: $id }) {
+ job {
+ ...Job
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
index 68c6584cda6..c8763d4767e 100644
--- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql
@@ -69,6 +69,7 @@ query getJobs(
stuck
userPermissions {
readBuild
+ readJobArtifacts
}
}
}
diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js
index 05d6ebfd6d6..f24daf90815 100644
--- a/app/assets/javascripts/jobs/components/table/index.js
+++ b/app/assets/javascripts/jobs/components/table/index.js
@@ -1,9 +1,12 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(VueApollo);
+Vue.use(GlToast);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -22,6 +25,7 @@ export default (containerId = 'js-jobs-table') => {
jobStatuses,
pipelineEditorPath,
emptyStateSvgPath,
+ admin,
} = containerEl.dataset;
return new Vue({
@@ -33,6 +37,7 @@ export default (containerId = 'js-jobs-table') => {
pipelineEditorPath,
jobStatuses: JSON.parse(jobStatuses),
jobCounts: JSON.parse(jobCounts),
+ admin: parseBoolean(admin),
},
render(createElement) {
return createElement(JobsTableApp);
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue
index 076c0e78b11..298c99c4162 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue
@@ -141,7 +141,7 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <actions-cell :job="item" />
+ <actions-cell class="gl-float-right" :job="item" />
</template>
</gl-table>
</template>
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 2061b1f1eb2..c786d35ac68 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -2,6 +2,7 @@
import { GlAlert, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import { __ } from '~/locale';
import { GRAPHQL_PAGE_SIZE, initialPaginationState } from './constants';
+import eventHub from './event_hub';
import GetJobs from './graphql/queries/get_jobs.query.graphql';
import JobsTable from './jobs_table.vue';
import JobsTableEmptyState from './jobs_table_empty_state.vue';
@@ -74,7 +75,16 @@ export default {
return Boolean(this.prevPage || this.nextPage) && !this.$apollo.loading;
},
},
+ mounted() {
+ eventHub.$on('jobActionPerformed', this.handleJobAction);
+ },
+ beforeDestroy() {
+ eventHub.$off('jobActionPerformed', this.handleJobAction);
+ },
methods: {
+ handleJobAction() {
+ this.$apollo.queries.jobs.refetch({ statuses: this.scope });
+ },
fetchJobsByStatus(scope) {
this.scope = scope;
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index a62ab301227..68019a35dbb 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,13 +1,11 @@
/* eslint-disable func-names, no-underscore-dangle, no-new, consistent-return, no-shadow, no-param-reassign, no-lonely-if, no-empty */
/* global Issuable */
-/* global ListLabel */
import $ from 'jquery';
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 boardsStore from './boards/stores/boards_store';
import CreateLabelDropdown from './create_label';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
@@ -43,7 +41,6 @@ export default class LabelsSelect {
const $form = $dropdown.closest('form, .js-issuable-update');
const $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
const $value = $block.find('.value');
- const $dropdownMenu = $dropdown.parent().find('.dropdown-menu');
const $loading = $block.find('.block-loading').addClass('gl-display-none');
const fieldName = $dropdown.data('fieldName');
let initialSelected = $selectbox
@@ -341,15 +338,11 @@ export default class LabelsSelect {
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
- vue: $dropdown.hasClass('js-issue-board-sidebar'),
+ vue: false,
clicked(clickEvent) {
- const { $el, e, isMarking } = clickEvent;
+ const { e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
- const hideLoader = () => {
- $loading.addClass('gl-display-none');
- };
-
const page = $('body').attr('data-page');
const isIssueIndex = page === 'projects:issues:index';
const isMRIndex = page === 'projects:merge_requests:index';
@@ -375,40 +368,6 @@ export default class LabelsSelect {
}
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if ($el.hasClass('is-active')) {
- boardsStore.detail.issue.labels.push(
- new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color,
- textColor: '#fff',
- }),
- );
- } else {
- let { labels } = boardsStore.detail.issue;
- labels = labels.filter((selectedLabel) => selectedLabel.id !== label.id);
- boardsStore.detail.issue.labels = labels;
- }
-
- $loading.removeClass('gl-display-none');
- const oldLabels = boardsStore.detail.issue.labels;
-
- boardsStore.detail.issue
- .update($dropdown.attr('data-issue-update'))
- .then(() => {
- if (isScopedLabel(label)) {
- const prevIds = oldLabels.map((label) => label.id);
- const newIds = boardsStore.detail.issue.labels.map((label) => label.id);
- const differentIds = prevIds.filter((x) => !newIds.includes(x));
- $dropdown.data('marked', newIds);
- $dropdownMenu
- .find(differentIds.map((id) => `[data-label-id="${id}"]`).join(','))
- .removeClass('is-active');
- }
- })
- .then(hideLoader)
- .catch(hideLoader);
} else if (handleClick) {
e.preventDefault();
handleClick(label);
@@ -419,13 +378,6 @@ export default class LabelsSelect {
}
}
},
- opened() {
- if ($dropdown.hasClass('js-issue-board-sidebar')) {
- const previousSelection = $dropdown.attr('data-selected');
- this.selected = previousSelection ? previousSelection.split(',') : [];
- $dropdown.data('deprecatedJQueryDropdown').updateLabel();
- }
- },
preserveContext: true,
});
diff --git a/app/assets/javascripts/learn_gitlab/track_learn_gitlab.js b/app/assets/javascripts/learn_gitlab/track_learn_gitlab.js
deleted file mode 100644
index 305d130f10c..00000000000
--- a/app/assets/javascripts/learn_gitlab/track_learn_gitlab.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import Tracking from '~/tracking';
-
-export default function trackLearnGitlab(learnGitlabA) {
- Tracking.event('projects:learn_gitlab:index', 'page_init', {
- label: 'learn_gitlab',
- property: learnGitlabA
- ? 'Growth::Conversion::Experiment::LearnGitLabA'
- : 'Growth::Activation::Experiment::LearnGitLabB',
- });
-}
diff --git a/app/assets/javascripts/lib/apollo/instrumentation_link.js b/app/assets/javascripts/lib/apollo/instrumentation_link.js
new file mode 100644
index 00000000000..2ab364557b8
--- /dev/null
+++ b/app/assets/javascripts/lib/apollo/instrumentation_link.js
@@ -0,0 +1,29 @@
+import { ApolloLink } from 'apollo-link';
+import { memoize } from 'lodash';
+
+export const FEATURE_CATEGORY_HEADER = 'x-gitlab-feature-category';
+
+/**
+ * Returns the ApolloLink (or null) used to add instrumentation metadata to the GraphQL request.
+ *
+ * - The result will be null if the `feature_category` cannot be found.
+ * - The result is memoized since the `feature_category` is the same for the entire page.
+ */
+export const getInstrumentationLink = memoize(() => {
+ const { feature_category: featureCategory } = gon;
+
+ if (!featureCategory) {
+ return null;
+ }
+
+ return new ApolloLink((operation, forward) => {
+ operation.setContext(({ headers = {} }) => ({
+ headers: {
+ ...headers,
+ [FEATURE_CATEGORY_HEADER]: featureCategory,
+ },
+ }));
+
+ return forward(operation);
+ });
+});
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index a026f76e51b..d421d66981e 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -3,7 +3,7 @@ import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
const defaultConfig = {
// Safely allow SVG <use> tags
- ADD_TAGS: ['use'],
+ ADD_TAGS: ['use', 'gl-emoji'],
// 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'],
@@ -16,7 +16,7 @@ const getAllowedIconUrls = (gon = window.gon) =>
const isUrlAllowed = (url) => getAllowedIconUrls().some((allowedUrl) => url.startsWith(allowedUrl));
const isHrefSafe = (url) =>
- isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
+ isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL())) || url.match(/^#/);
const removeUnsafeHref = (node, attr) => {
if (!node.hasAttribute(attr)) {
@@ -52,4 +52,4 @@ addHook('afterSanitizeAttributes', (node) => {
}
});
-export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
+export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config });
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 0804213cafa..b96a55fe116 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -10,6 +10,7 @@ import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
+import { getInstrumentationLink } from './apollo/instrumentation_link';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -140,14 +141,17 @@ export default (resolvers = {}, config = {}) => {
const appLink = ApolloLink.split(
hasSubscriptionOperation,
new ActionCableLink(),
- ApolloLink.from([
- requestCounterLink,
- performanceBarLink,
- new StartupJSLink(),
- apolloCaptchaLink,
- uploadsLink,
- requestLink,
- ]),
+ ApolloLink.from(
+ [
+ getInstrumentationLink(),
+ requestCounterLink,
+ performanceBarLink,
+ new StartupJSLink(),
+ apolloCaptchaLink,
+ uploadsLink,
+ requestLink,
+ ].filter(Boolean),
+ ),
);
return new ApolloClient({
diff --git a/app/assets/javascripts/lib/logger/hello.js b/app/assets/javascripts/lib/logger/hello.js
new file mode 100644
index 00000000000..18fa35ab55b
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/hello.js
@@ -0,0 +1,16 @@
+const HANDSHAKE = String.fromCodePoint(0x1f91d);
+const MAG = String.fromCodePoint(0x1f50e);
+
+export const logHello = () => {
+ // eslint-disable-next-line no-console
+ console.log(
+ `%cWelcome to GitLab!%c
+
+Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!
+
+${HANDSHAKE} Contribute to GitLab: https://about.gitlab.com/community/contribute/
+${MAG} Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new`,
+ `padding-top: 0.5em; font-size: 2em;`,
+ 'padding-bottom: 0.5em;',
+ );
+};
diff --git a/app/assets/javascripts/lib/logger/hello_deferred.js b/app/assets/javascripts/lib/logger/hello_deferred.js
new file mode 100644
index 00000000000..ce1dd91cb37
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/hello_deferred.js
@@ -0,0 +1,5 @@
+export const logHelloDeferred = async () => {
+ const { logHello } = await import(/* webpackChunkName: 'hello' */ './hello');
+
+ logHello();
+};
diff --git a/app/assets/javascripts/lib/logger/index.js b/app/assets/javascripts/lib/logger/index.js
new file mode 100644
index 00000000000..0f5353fcbed
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/index.js
@@ -0,0 +1,6 @@
+/* eslint-disable no-console */
+export const LOG_PREFIX = '[gitlab]';
+
+export const logError = (message = '', ...args) => {
+ console.error(LOG_PREFIX, `${message}\n`, ...args);
+};
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js
index 39cffedcac6..d4a6d70c62c 100644
--- a/app/assets/javascripts/lib/utils/accessor.js
+++ b/app/assets/javascripts/lib/utils/accessor.js
@@ -1,4 +1,4 @@
-function isPropertyAccessSafe(base, property) {
+function canAccessProperty(base, property) {
let safe;
try {
@@ -10,7 +10,7 @@ function isPropertyAccessSafe(base, property) {
return safe;
}
-function isFunctionCallSafe(base, functionName, ...args) {
+function canCallFunction(base, functionName, ...args) {
let safe = true;
try {
@@ -22,16 +22,28 @@ function isFunctionCallSafe(base, functionName, ...args) {
return safe;
}
-function isLocalStorageAccessSafe() {
+/**
+ * Determines if `window.localStorage` is available and
+ * can be written to and read from.
+ *
+ * Important: This is not a guarantee that
+ * `localStorage.setItem` will work in all cases.
+ *
+ * `setItem` can still throw exceptions and should be
+ * surrounded with a try/catch where used.
+ *
+ * See: https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#exceptions
+ */
+function canUseLocalStorage() {
let safe;
- const TEST_KEY = 'isLocalStorageAccessSafe';
+ const TEST_KEY = 'canUseLocalStorage';
const TEST_VALUE = 'true';
- safe = isPropertyAccessSafe(window, 'localStorage');
+ safe = canAccessProperty(window, 'localStorage');
if (!safe) return safe;
- safe = isFunctionCallSafe(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
+ safe = canCallFunction(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
if (safe) window.localStorage.removeItem(TEST_KEY);
@@ -39,9 +51,7 @@ function isLocalStorageAccessSafe() {
}
const AccessorUtilities = {
- isPropertyAccessSafe,
- isFunctionCallSafe,
- isLocalStorageAccessSafe,
+ canUseLocalStorage,
};
export default AccessorUtilities;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8f86fd55d6e..fd9629499b0 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -117,7 +117,6 @@ export const handleLocationHash = () => {
};
// Check if element scrolled into viewport from above or below
-// Courtesy http://stackoverflow.com/a/7557433/414749
export const isInViewport = (el, offset = {}) => {
const rect = el.getBoundingClientRect();
const { top, left } = offset;
@@ -560,11 +559,9 @@ export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
* Method to round of values with decimal places
* with provided precision.
*
- * Taken from https://stackoverflow.com/a/7343013/414749
- *
* Eg; roundOffFloat(3.141592, 3) = 3.142
*
- * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
+ * Refer to spec/frontend/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
@@ -581,7 +578,7 @@ export const roundOffFloat = (number, precision = 0) => {
*
* Eg; roundToNearestHalf(3.141592) = 3, roundToNearestHalf(3.41592) = 3.5
*
- * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
+ * Refer to spec/frontend/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
@@ -595,7 +592,7 @@ export const roundToNearestHalf = (num) => Math.round(num * 2).toFixed() / 2;
*
* Eg; roundDownFloat(3.141592, 3) = 3.141
*
- * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
+ * Refer to spec/frontend/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
@@ -645,7 +642,7 @@ export const NavigationType = {
* matched with our query.
*
* You can learn more about behaviour of this method by referring to tests
- * within `spec/javascripts/lib/utils/common_utils_spec.js`.
+ * within `spec/frontend/lib/utils/common_utils_spec.js`.
*
* @param {string} query String to search for
* @param {object} searchSpace Object containing properties to search in for `query`
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 246f290a90a..0a35efb0ac8 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -1,5 +1,5 @@
import dateFormat from 'dateformat';
-import { isString, mapValues, reduce } from 'lodash';
+import { isString, mapValues, reduce, isDate } from 'lodash';
import { s__, n__, __ } from '../../../locale';
/**
@@ -258,3 +258,106 @@ export const parseSeconds = (
return periodCount;
});
};
+
+/**
+ * Pads given items with zeros to reach a length of 2 characters.
+ *
+ * @param {...any} args Items to be padded.
+ * @returns {Array<String>} Padded items.
+ */
+export const padWithZeros = (...args) => args.map((arg) => `${arg}`.padStart(2, '0'));
+
+/**
+ * This removes the timezone from an ISO date string.
+ * This can be useful when populating date/time fields along with a distinct timezone selector, in
+ * which case we'd want to ignore the timezone's offset when populating the date and time.
+ *
+ * Examples:
+ * stripTimezoneFromISODate('2021-08-16T00:00:00.000-02:00') => '2021-08-16T00:00:00.000'
+ * stripTimezoneFromISODate('2021-08-16T00:00:00.000Z') => '2021-08-16T00:00:00.000'
+ *
+ * @param {String} date The ISO date string representation.
+ * @returns {String} The ISO date string without the timezone.
+ */
+export const stripTimezoneFromISODate = (date) => {
+ if (Number.isNaN(Date.parse(date))) {
+ return null;
+ }
+ return date.replace(/(Z|[+-]\d{2}:\d{2})$/, '');
+};
+
+/**
+ * Extracts the year, month and day from a Date instance and returns them in an object.
+ * For example:
+ * dateToYearMonthDate(new Date('2021-08-16')) => { year: '2021', month: '08', day: '16' }
+ *
+ * @param {Date} date The date to be parsed
+ * @returns {Object} An object containing the extracted year, month and day.
+ */
+export const dateToYearMonthDate = (date) => {
+ if (!isDate(date)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Argument should be a Date instance');
+ }
+ const [month, day] = padWithZeros(date.getMonth() + 1, date.getDate());
+ return {
+ year: `${date.getFullYear()}`,
+ month,
+ day,
+ };
+};
+
+/**
+ * Extracts the hours and minutes from a string representing a time.
+ * For example:
+ * timeToHoursMinutes('12:46') => { hours: '12', minutes: '46' }
+ *
+ * @param {String} time The time to be parsed in the form HH:MM.
+ * @returns {Object} An object containing the hours and minutes.
+ */
+export const timeToHoursMinutes = (time = '') => {
+ if (!time || !time.match(/\d{1,2}:\d{1,2}/)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Invalid time provided');
+ }
+ const [hours, minutes] = padWithZeros(...time.split(':'));
+ return { hours, minutes };
+};
+
+/**
+ * This combines a date and a time and returns the computed Date's ISO string representation.
+ *
+ * @param {Date} date Date object representing the base date.
+ * @param {String} time String representing the time to be used, in the form HH:MM.
+ * @param {String} offset An optional Date-compatible offset.
+ * @returns {String} The combined Date's ISO string representation.
+ */
+export const dateAndTimeToISOString = (date, time, offset = '') => {
+ const { year, month, day } = dateToYearMonthDate(date);
+ const { hours, minutes } = timeToHoursMinutes(time);
+ const dateString = `${year}-${month}-${day}T${hours}:${minutes}:00.000${offset || 'Z'}`;
+ if (Number.isNaN(Date.parse(dateString))) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Could not initialize date');
+ }
+ return dateString;
+};
+
+/**
+ * Converts a Date instance to time input-compatible value consisting in a 2-digits hours and
+ * minutes, separated by a semi-colon, in the 24-hours format.
+ *
+ * @param {Date} date Date to be converted
+ * @returns {String} time input-compatible string in the form HH:MM.
+ */
+export const dateToTimeInputValue = (date) => {
+ if (!isDate(date)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Argument should be a Date instance');
+ }
+ return date.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index f11c7658a88..f7687a929de 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -77,3 +77,15 @@ export const isElementVisible = (element) =>
* @returns {Boolean} `true` if the element is currently hidden, otherwise false
*/
export const isElementHidden = (element) => !isElementVisible(element);
+
+export const getParents = (element) => {
+ const parents = [];
+ let parent = element.parentNode;
+
+ do {
+ parents.push(parent);
+ parent = parent.parentNode;
+ } while (parent);
+
+ return parents;
+};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index f3dedb7726a..f46263c0e4d 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -69,19 +69,20 @@ export function bytesToGiB(number) {
* representation (e.g., giving it 1500 yields 1.5 KB).
*
* @param {Number} size
+ * @param {Number} digits - The number of digits to appear after the decimal point
* @returns {String}
*/
-export function numberToHumanSize(size) {
+export function numberToHumanSize(size, digits = 2) {
const abs = Math.abs(size);
if (abs < BYTES_IN_KIB) {
return sprintf(__('%{size} bytes'), { size });
} else if (abs < BYTES_IN_KIB ** 2) {
- return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(2) });
+ return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(digits) });
} else if (abs < BYTES_IN_KIB ** 3) {
- return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(2) });
+ return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(digits) });
}
- return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(2) });
+ return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(digits) });
}
/**
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 6ff2af47dd8..0804d792631 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -232,7 +232,9 @@ export function insertMarkdownText({
.join('\n');
}
} else if (tag.indexOf(textPlaceholder) > -1) {
- textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
+ textToInsert = tag.replace(textPlaceholder, () =>
+ selected.replace(/\\n/g, '\n').replace('%br', '\\n'),
+ );
} else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index e9772232eaf..bca0e45d98d 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -418,43 +418,6 @@ export const urlParamsToArray = (path = '') =>
export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
/**
- * Accepts encoding string which includes query params being
- * sent to URL.
- *
- * @param {string} path Query param string
- *
- * @returns {object} Query params object containing key-value pairs
- * with both key and values decoded into plain string.
- *
- * @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845
- */
-export const urlParamsToObject = (path = '') =>
- splitPath(path).reduce((dataParam, filterParam) => {
- if (filterParam === '') {
- return dataParam;
- }
-
- const data = dataParam;
- let [key, value] = filterParam.split('=');
- key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
- const isArray = key.includes('[]');
- key = key.replace('[]', '');
- value = decodeURIComponent(value.replace(/\+/g, ' '));
-
- if (isArray) {
- if (!data[key]) {
- data[key] = [];
- }
-
- data[key].push(value);
- } else {
- data[key] = value;
- }
-
- return data;
- }, {});
-
-/**
* Convert search query into an object
*
* @param {String} query from "document.location.search"
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 1aaefcaa13b..b96a2607552 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -19,6 +19,7 @@ import initAlertHandler from './alert_handler';
import { removeFlashClickListener } from './flash';
import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
+import { logHelloDeferred } from './lib/logger/hello_deferred';
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime/timeago_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
@@ -35,8 +36,12 @@ import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
+import { initHeaderSearchApp } from '~/header_search';
import 'ee_else_ce/main_ee';
+import 'jh_else_ce/main_jh';
+
+logHelloDeferred();
applyGitLabUIConfig();
@@ -94,20 +99,24 @@ function deferredInitialisation() {
initDefaultTrackers();
initFeatureHighlight();
- const search = document.querySelector('#search');
- if (search) {
- search.addEventListener(
- 'focus',
- () => {
- import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete')
- .then(({ default: initSearchAutocomplete }) => {
- const searchDropdown = initSearchAutocomplete();
- searchDropdown.onSearchInputFocus();
- })
- .catch(() => {});
- },
- { once: true },
- );
+ if (gon.features?.newHeaderSearch) {
+ initHeaderSearchApp();
+ } else {
+ const search = document.querySelector('#search');
+ if (search) {
+ search.addEventListener(
+ 'focus',
+ () => {
+ import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete')
+ .then(({ default: initSearchAutocomplete }) => {
+ const searchDropdown = initSearchAutocomplete();
+ searchDropdown.onSearchInputFocus();
+ })
+ .catch(() => {});
+ },
+ { once: true },
+ );
+ }
}
addSelectOnFocusBehaviour('.js-select-on-focus');
diff --git a/app/assets/javascripts/main_jh.js b/app/assets/javascripts/main_jh.js
new file mode 100644
index 00000000000..13a6b8f3d3d
--- /dev/null
+++ b/app/assets/javascripts/main_jh.js
@@ -0,0 +1 @@
+// This is an empty file to satisfy jh_else_ce import for the JH main entry point
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 0ddb2c2334c..ed32f26583e 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -19,11 +19,9 @@ function MergeRequest(opts) {
this.opts = opts != null ? opts : {};
this.submitNoteForm = this.submitNoteForm.bind(this);
this.$el = $('.merge-request');
- this.$('.show-all-commits').on('click', () => this.showAllCommits());
this.initTabs();
this.initMRBtnListeners();
- this.initCommitMessageListeners();
if ($('.description.js-task-list-container').length) {
this.taskList = new TaskList({
@@ -59,11 +57,6 @@ MergeRequest.prototype.initTabs = function () {
window.mrTabs = new MergeRequestTabs(this.opts);
};
-MergeRequest.prototype.showAllCommits = function () {
- this.$('.first-commits').remove();
- return this.$('.all-commits').removeClass('hide');
-};
-
MergeRequest.prototype.initMRBtnListeners = function () {
const _this = this;
const draftToggles = document.querySelectorAll('.js-draft-toggle-button');
@@ -128,26 +121,6 @@ MergeRequest.prototype.submitNoteForm = function (form, $button) {
}
};
-MergeRequest.prototype.initCommitMessageListeners = function () {
- $(document).on('click', 'a.js-with-description-link', (e) => {
- const textarea = $('textarea.js-commit-message');
- e.preventDefault();
-
- textarea.val(textarea.data('messageWithDescription'));
- $('.js-with-description-hint').hide();
- $('.js-without-description-hint').show();
- });
-
- $(document).on('click', 'a.js-without-description-link', (e) => {
- const textarea = $('textarea.js-commit-message');
- e.preventDefault();
-
- textarea.val(textarea.data('messageWithoutDescription'));
- $('.js-with-description-hint').show();
- $('.js-without-description-hint').hide();
- });
-};
-
MergeRequest.decreaseCounter = function (by = 1) {
const $el = $('.js-merge-counter');
const count = Math.max(parseInt($el.text().replace(/[^\d]/, ''), 10) - by, 0);
@@ -164,7 +137,7 @@ MergeRequest.hideCloseButton = function () {
MergeRequest.toggleDraftStatus = function (title, isReady) {
if (isReady) {
createFlash({
- message: __('The merge request can now be merged.'),
+ message: __('Marked as ready. Merging is now allowed.'),
type: 'notice',
});
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 14e5e96d7b0..a40caea1223 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -7,20 +7,17 @@ import createEventHub from '~/helpers/event_hub_factory';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
import createFlash from './flash';
-import initChangesDropdown from './init_changes_dropdown';
+import { initDiffStatsDropdown } from './init_diff_stats_dropdown';
import axios from './lib/utils/axios_utils';
import {
parseUrlPathname,
- handleLocationHash,
isMetaClick,
parseBoolean,
scrollToElement,
} from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
-import { getLocationHash } from './lib/utils/url_utility';
import { __ } from './locale';
-import Notes from './notes';
import syntaxHighlight from './syntax_highlight';
// MergeRequestTabs
@@ -67,6 +64,8 @@ import syntaxHighlight from './syntax_highlight';
// </div>
//
+// <100ms is typically indistinguishable from "instant" for users, but allows for re-rendering
+const FAST_DELAY_FOR_RERENDER = 75;
// Store the `location` object, allowing for easier stubbing in tests
let { location } = window;
@@ -86,6 +85,8 @@ export default class MergeRequestTabs {
this.peek = document.getElementById('js-peek');
this.paddingTop = 16;
+ this.scrollPositions = {};
+
this.commitsTab = document.querySelector('.tab-content .commits.tab-pane');
this.currentTab = null;
@@ -139,11 +140,30 @@ export default class MergeRequestTabs {
}
}
+ storeScroll() {
+ if (this.currentTab) {
+ this.scrollPositions[this.currentTab] = document.documentElement.scrollTop;
+ }
+ }
+ recallScroll(action) {
+ const storedPosition = this.scrollPositions[action];
+
+ setTimeout(() => {
+ window.scrollTo({
+ top: storedPosition && storedPosition > 0 ? storedPosition : 0,
+ left: 0,
+ behavior: 'auto',
+ });
+ }, FAST_DELAY_FOR_RERENDER);
+ }
+
clickTab(e) {
if (e.currentTarget) {
e.stopImmediatePropagation();
e.preventDefault();
+ this.storeScroll();
+
const { action } = e.currentTarget.dataset || {};
if (isMetaClick(e)) {
@@ -193,6 +213,14 @@ export default class MergeRequestTabs {
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) {
if (!isInVueNoteablePage()) {
+ /*
+ for pages where we have not yet converted to the new vue
+ implementation we load the diff tab content the old way,
+ inserting html rendered by the backend.
+
+ in practice, this only occurs when comparing commits in
+ the new merge request form page.
+ */
this.loadDiff(href);
}
if (bp.getBreakpointSize() !== 'xl') {
@@ -205,8 +233,14 @@ export default class MergeRequestTabs {
this.resetViewContainer();
this.mountPipelinesView();
} else {
- this.mergeRequestTabPanes.querySelector('#notes').style.display = 'block';
- this.mergeRequestTabs.querySelector('.notes-tab').classList.add('active');
+ const notesTab = this.mergeRequestTabs.querySelector('.notes-tab');
+ const notesPane = this.mergeRequestTabPanes.querySelector('#notes');
+ if (notesPane) {
+ notesPane.style.display = 'block';
+ }
+ if (notesTab) {
+ notesTab.classList.add('active');
+ }
if (bp.getBreakpointSize() !== 'xs') {
this.expandView();
@@ -216,6 +250,8 @@ export default class MergeRequestTabs {
}
$('.detail-page-description').renderGFM();
+
+ this.recallScroll(action);
} else if (action === this.currentAction) {
// ContentTop is used to handle anything at the top of the page before the main content
const mainContentContainer = document.querySelector('.content-wrapper');
@@ -379,6 +415,7 @@ export default class MergeRequestTabs {
pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el);
}
+ // load the diff tab content from the backend
loadDiff(source) {
if (this.diffsLoaded) {
document.dispatchEvent(new CustomEvent('scroll'));
@@ -396,8 +433,7 @@ export default class MergeRequestTabs {
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
-
- initChangesDropdown(this.stickyTop);
+ initDiffStatsDropdown(this.stickyTop);
localTimeAgo(document.querySelectorAll('#diffs .js-timeago'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
@@ -420,25 +456,6 @@ export default class MergeRequestTabs {
}).init();
});
- // Scroll any linked note into view
- // Similar to `toggler_behavior` in the discussion tab
- const hash = getLocationHash();
- const anchor = hash && $container.find(`.note[id="${hash}"]`);
- if (anchor && anchor.length > 0) {
- const notesContent = anchor.closest('.notes-content');
- const lineType = notesContent.hasClass('new') ? 'new' : 'old';
- Notes.instance.toggleDiffNote({
- target: anchor,
- lineType,
- forceShow: true,
- });
- anchor[0].scrollIntoView();
- handleLocationHash();
- // We have multiple elements on the page with `#note_xxx`
- // (discussion and diff tabs) and `:target` only applies to the first
- anchor.addClass('target');
- }
-
this.toggleLoading(false);
})
.catch(() => {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 0d9a2eef01a..aa8a40b6a87 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,6 +1,5 @@
/* eslint-disable one-var, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
-/* global ListMilestone */
import $ from 'jquery';
import { template, escape } from 'lodash';
@@ -8,10 +7,6 @@ import Api from '~/api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { __, sprintf } from '~/locale';
import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
-import boardsStore, {
- boardStoreIssueSet,
- boardStoreIssueDelete,
-} from './boards/stores/boards_store';
import axios from './lib/utils/axios_utils';
import { timeFor, parsePikadayDate, dateInWords } from './lib/utils/datetime_utility';
@@ -186,18 +181,17 @@ export default class MilestoneSelect {
},
opened: (e) => {
const $el = $(e.currentTarget);
- if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
+ if (options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
- vue: $dropdown.hasClass('js-issue-board-sidebar'),
+ vue: false,
clicked: (clickEvent) => {
const { e } = clickEvent;
let selected = clickEvent.selectedObj;
- let data;
if (!selected) return;
if (options.handleClick) {
@@ -224,76 +218,52 @@ export default class MilestoneSelect {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- if (selected.id !== -1 && isSelecting) {
- boardStoreIssueSet(
- 'milestone',
- new ListMilestone({
- id: selected.id,
- title: selected.name,
- }),
- );
- } else {
- boardStoreIssueDelete('milestone');
- }
+ }
- $dropdown.trigger('loading.gl.dropdown');
- $loading.removeClass('gl-display-none');
+ selected = $selectBox.find('input[type="hidden"]').val();
- boardsStore.detail.issue
- .update($dropdown.attr('data-issue-update'))
- .then(() => {
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.addClass('gl-display-none');
- })
- .catch(() => {
- $loading.addClass('gl-display-none');
- });
- } else {
- selected = $selectBox.find('input[type="hidden"]').val();
- data = {};
- data[abilityName] = {};
- data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.removeClass('gl-display-none');
- $dropdown.trigger('loading.gl.dropdown');
- return axios
- .put(issueUpdateURL, data)
- .then(({ data }) => {
- $dropdown.trigger('loaded.gl.dropdown');
- $loading.addClass('gl-display-none');
- $selectBox.hide();
- $value.css('display', '');
- if (data.milestone != null) {
- data.milestone.remaining = timeFor(data.milestone.due_date);
- data.milestone.name = data.milestone.title;
- $value.html(
- data.milestone.expired
- ? milestoneExpiredLinkTemplate({
- ...data.milestone,
- remaining: sprintf(__('%{due_date} (Past due)'), {
- due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
- }),
- })
- : milestoneLinkTemplate(data.milestone),
- );
- return $sidebarCollapsedValue
- .attr(
- 'data-original-title',
- `${data.milestone.name}<br />${data.milestone.remaining}`,
- )
- .find('span')
- .text(data.milestone.title);
- }
- $value.html(milestoneLinkNoneTemplate);
+ const data = {};
+ data[abilityName] = {};
+ data[abilityName].milestone_id = selected != null ? selected : null;
+ $loading.removeClass('gl-display-none');
+ $dropdown.trigger('loading.gl.dropdown');
+ return axios
+ .put(issueUpdateURL, data)
+ .then(({ data }) => {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.addClass('gl-display-none');
+ $selectBox.hide();
+ $value.css('display', '');
+ if (data.milestone != null) {
+ data.milestone.remaining = timeFor(data.milestone.due_date);
+ data.milestone.name = data.milestone.title;
+ $value.html(
+ data.milestone.expired
+ ? milestoneExpiredLinkTemplate({
+ ...data.milestone,
+ remaining: sprintf(__('%{due_date} (Past due)'), {
+ due_date: dateInWords(parsePikadayDate(data.milestone.due_date)),
+ }),
+ })
+ : milestoneLinkTemplate(data.milestone),
+ );
return $sidebarCollapsedValue
- .attr('data-original-title', __('Milestone'))
+ .attr(
+ 'data-original-title',
+ `${data.milestone.name}<br />${data.milestone.remaining}`,
+ )
.find('span')
- .text(__('None'));
- })
- .catch(() => {
- $loading.addClass('gl-display-none');
- });
- }
+ .text(data.milestone.title);
+ }
+ $value.html(milestoneLinkNoneTemplate);
+ return $sidebarCollapsedValue
+ .attr('data-original-title', __('Milestone'))
+ .find('span')
+ .text(__('None'));
+ })
+ .catch(() => {
+ $loading.addClass('gl-display-none');
+ });
},
});
});
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index e8499015210..a840e696386 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -125,8 +125,7 @@ export default {
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
- // made inaccessible by Vue. More info:
- // https://stackoverflow.com/a/52988020/1063392
+ // made inaccessible by Vue.
this.debouncedSearch = debounce(function search() {
this.search(this.searchQuery);
}, SEARCH_DEBOUNCE_MS);
diff --git a/app/assets/javascripts/milestones/stores/mutations.js b/app/assets/javascripts/milestones/stores/mutations.js
index 3a7babf6fa0..1f88c0a1ea6 100644
--- a/app/assets/javascripts/milestones/stores/mutations.js
+++ b/app/assets/javascripts/milestones/stores/mutations.js
@@ -38,7 +38,7 @@ export default {
[types.RECEIVE_PROJECT_MILESTONES_SUCCESS](state, response) {
state.matches.projectMilestones = {
list: response.data.map(({ title }) => ({ title })),
- totalCount: parseInt(response.headers['x-total'], 10),
+ totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
},
@@ -52,7 +52,7 @@ export default {
[types.RECEIVE_GROUP_MILESTONES_SUCCESS](state, response) {
state.matches.groupMilestones = {
list: response.data.map(({ title }) => ({ title })),
- totalCount: parseInt(response.headers['x-total'], 10),
+ totalCount: parseInt(response.headers['x-total'], 10) || response.data.length,
error: null,
};
},
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index 446ca8e5090..4b54cffe231 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
import { chartHeight } from '../../constants';
@@ -26,7 +25,7 @@ export default {
<div
class="gl-mt-3 svg-w-100 d-flex align-items-center"
:style="svgContainerStyle"
- v-html="chartEmptyStateIllustration"
+ v-html="chartEmptyStateIllustration /* eslint-disable-line vue/no-v-html */"
></div>
<h5 class="text-center gl-mt-3">{{ __('No data to display') }}</h5>
</div>
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index a7696a716d0..ea3e4e5604c 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -19,6 +19,7 @@ export default function initMrNotes() {
action: mrShowNode.dataset.mrAction,
});
+ initDiffsApp(store);
initNotesApp();
document.addEventListener('merged:UpdateActions', () => {
@@ -26,20 +27,25 @@ export default function initMrNotes() {
initCherryPickCommitModal();
});
- // eslint-disable-next-line no-new
- new Vue({
- el: '#js-vue-discussion-counter',
- name: 'DiscussionCounter',
- components: {
- discussionCounter,
- },
- store,
- render(createElement) {
- return createElement('discussion-counter');
- },
- });
+ requestIdleCallback(() => {
+ const el = document.getElementById('js-vue-discussion-counter');
- initDiscussionFilters(store);
- initSortDiscussions(store);
- initDiffsApp(store);
+ if (el) {
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'DiscussionCounter',
+ components: {
+ discussionCounter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-counter');
+ },
+ });
+ }
+
+ initDiscussionFilters(store);
+ initSortDiscussions(store);
+ });
}
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 0f4cec67ce8..1384c9c40b3 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import katex from 'katex';
import marked from 'marked';
import { sanitize } from '~/lib/dompurify';
@@ -95,7 +94,16 @@ renderer.image = function image(href, title, text) {
const attachmentHeader = `attachment:`; // eslint-disable-line @gitlab/require-i18n-strings
if (!this.attachments || !href.startsWith(attachmentHeader)) {
- return this.originalImage(href, title, text);
+ let relativeHref = href;
+
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (!(href.startsWith('http') || href.startsWith('data:'))) {
+ // These are images within the repo. This will only work if the image
+ // is relative to the path where the file is located
+ relativeHref = this.relativeRawPath + href;
+ }
+
+ return this.originalImage(relativeHref, title, text);
}
let img = ``;
@@ -130,6 +138,7 @@ export default {
components: {
prompt: Prompt,
},
+ inject: ['relativeRawPath'],
props: {
cell: {
type: Object,
@@ -139,6 +148,7 @@ export default {
computed: {
markdown() {
renderer.attachments = this.cell.attachments;
+ renderer.relativeRawPath = this.relativeRawPath;
return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), markdownConfig);
},
@@ -149,7 +159,7 @@ export default {
<template>
<div class="cell text-cell">
<prompt />
- <div class="markdown" v-html="markdown"></div>
+ <div class="markdown" v-html="markdown /* eslint-disable-line vue/no-v-html */"></div>
</div>
</template>
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index dc5b2b66348..ca02ee18dd1 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,6 +1,5 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
-import { sanitize } from '~/lib/dompurify';
import Prompt from '../prompt.vue';
export default {
@@ -25,19 +24,19 @@ export default {
},
},
computed: {
- sanitizedOutput() {
- return sanitize(this.rawCode);
- },
showOutput() {
return this.index === 0;
},
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
};
</script>
<template>
<div class="output">
<prompt type="Out" :count="count" :show-output="showOutput" />
- <div v-safe-html="sanitizedOutput" class="gl-overflow-auto"></div>
+ <div v-safe-html:[$options.safeHtmlConfig]="rawCode" class="gl-overflow-auto"></div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 2ebebd76e1e..4e31fdcd4f0 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -1,14 +1,5 @@
<script>
-import {
- GlAlert,
- GlButton,
- GlIcon,
- GlFormCheckbox,
- GlTooltipDirective,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
-} from '@gitlab/ui';
+import { GlAlert, GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import Autosize from 'autosize';
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
@@ -34,6 +25,7 @@ import { COMMENT_FORM } from '../i18n';
import issuableStateMixin from '../mixins/issuable_state';
import CommentFieldLayout from './comment_field_layout.vue';
+import CommentTypeDropdown from './comment_type_dropdown.vue';
import discussionLockedWidget from './discussion_locked_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
@@ -42,8 +34,6 @@ const { UNPROCESSABLE_ENTITY } = httpStatusCodes;
export default {
name: 'CommentForm',
i18n: COMMENT_FORM,
- noteTypeComment: constants.COMMENT,
- noteTypeDiscussion: constants.DISCUSSION,
components: {
noteSignedOutWidget,
discussionLockedWidget,
@@ -53,10 +43,8 @@ export default {
TimelineEntryItem,
GlIcon,
CommentFieldLayout,
+ CommentTypeDropdown,
GlFormCheckbox,
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -88,12 +76,6 @@ export default {
'hasDrafts',
]),
...mapState(['isToggleStateButtonLoading']),
- isNoteTypeComment() {
- return this.noteType === constants.COMMENT;
- },
- isNoteTypeDiscussion() {
- return this.noteType === constants.DISCUSSION;
- },
noteableDisplayName() {
return splitCamelCase(this.noteableType).toLowerCase();
},
@@ -105,15 +87,8 @@ export default {
? this.$options.i18n.comment
: this.$options.i18n.startThread;
},
- startDiscussionDescription() {
- return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE
- ? this.$options.i18n.discussionThatNeedsResolution
- : this.$options.i18n.discussion;
- },
- commentDescription() {
- return sprintf(this.$options.i18n.submitButton.commentHelp, {
- noteableDisplayName: this.noteableDisplayName,
- });
+ discussionsRequireResolution() {
+ return this.getNoteableData.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE;
},
isOpen() {
return this.openState === constants.OPENED || this.openState === constants.REOPENED;
@@ -314,15 +289,6 @@ export default {
this.autosave.reset();
},
- setNoteType(type) {
- this.noteType = type;
- },
- setNoteTypeToComment() {
- this.setNoteType(constants.COMMENT);
- },
- setNoteTypeToDiscussion() {
- this.setNoteType(constants.DISCUSSION);
- },
editCurrentUserLastNote() {
if (this.note === '') {
const lastNote = this.getCurrentUserLastNote;
@@ -448,40 +414,15 @@ export default {
class="gl-text-gray-500"
/>
</gl-form-checkbox>
- <gl-dropdown
- split
- :text="commentButtonTitle"
- class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown"
- category="primary"
- variant="confirm"
+ <comment-type-dropdown
+ v-model="noteType"
+ class="gl-mr-3"
:disabled="disableSubmitButton"
- data-testid="comment-button"
- data-qa-selector="comment_button"
- :data-track-label="trackingLabel"
- data-track-event="click_button"
- @click="handleSave()"
- >
- <gl-dropdown-item
- is-check-item
- :is-checked="isNoteTypeComment"
- :selected="isNoteTypeComment"
- @click="setNoteTypeToComment"
- >
- <strong>{{ $options.i18n.submitButton.comment }}</strong>
- <p class="gl-m-0">{{ commentDescription }}</p>
- </gl-dropdown-item>
- <gl-dropdown-divider />
- <gl-dropdown-item
- is-check-item
- :is-checked="isNoteTypeDiscussion"
- :selected="isNoteTypeDiscussion"
- data-qa-selector="discussion_menu_item"
- @click="setNoteTypeToDiscussion"
- >
- <strong>{{ $options.i18n.submitButton.startThread }}</strong>
- <p class="gl-m-0">{{ startDiscussionDescription }}</p>
- </gl-dropdown-item>
- </gl-dropdown>
+ :tracking-label="trackingLabel"
+ :noteable-display-name="noteableDisplayName"
+ :discussions-require-resolution="discussionsRequireResolution"
+ @click="handleSave"
+ />
</template>
<gl-button
v-if="canToggleIssueState"
diff --git a/app/assets/javascripts/notes/components/comment_type_dropdown.vue b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
new file mode 100644
index 00000000000..663a912999d
--- /dev/null
+++ b/app/assets/javascripts/notes/components/comment_type_dropdown.vue
@@ -0,0 +1,114 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+
+import { sprintf } from '~/locale';
+import { COMMENT_FORM } from '~/notes/i18n';
+import * as constants from '../constants';
+
+export default {
+ i18n: COMMENT_FORM,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ },
+ model: {
+ prop: 'noteType',
+ event: 'change',
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ trackingLabel: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ discussionsRequireResolution: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ noteableDisplayName: {
+ type: String,
+ required: true,
+ },
+ noteType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isNoteTypeComment() {
+ return this.noteType === constants.COMMENT;
+ },
+ isNoteTypeDiscussion() {
+ return this.noteType === constants.DISCUSSION;
+ },
+ commentButtonTitle() {
+ return this.noteType === constants.COMMENT
+ ? this.$options.i18n.comment
+ : this.$options.i18n.startThread;
+ },
+ startDiscussionDescription() {
+ return this.discussionsRequireResolution
+ ? this.$options.i18n.discussionThatNeedsResolution
+ : this.$options.i18n.discussion;
+ },
+ commentDescription() {
+ return sprintf(this.$options.i18n.submitButton.commentHelp, {
+ noteableDisplayName: this.noteableDisplayName,
+ });
+ },
+ },
+ methods: {
+ handleClick() {
+ this.$emit('click');
+ },
+ setNoteTypeToComment() {
+ if (this.noteType !== constants.COMMENT) {
+ this.$emit('change', constants.COMMENT);
+ }
+ },
+ setNoteTypeToDiscussion() {
+ if (this.noteType !== constants.DISCUSSION) {
+ this.$emit('change', constants.DISCUSSION);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ split
+ :text="commentButtonTitle"
+ class="gl-mr-3 js-comment-button js-comment-submit-button comment-type-dropdown"
+ category="primary"
+ variant="confirm"
+ :disabled="disabled"
+ data-testid="comment-button"
+ data-qa-selector="comment_button"
+ :data-track-label="trackingLabel"
+ data-track-action="click_button"
+ @click="$emit('click')"
+ >
+ <gl-dropdown-item is-check-item :is-checked="isNoteTypeComment" @click="setNoteTypeToComment">
+ <strong>{{ $options.i18n.submitButton.comment }}</strong>
+ <p class="gl-m-0">{{ commentDescription }}</p>
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ <gl-dropdown-item
+ is-check-item
+ :is-checked="isNoteTypeDiscussion"
+ data-qa-selector="discussion_menu_item"
+ @click="setNoteTypeToDiscussion"
+ >
+ <strong>{{ $options.i18n.submitButton.startThread }}</strong>
+ <p class="gl-m-0">{{ startDiscussionDescription }}</p>
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index e96e1204f76..b04aa74d46e 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
@@ -93,7 +92,11 @@ export default {
>
<td :class="line.type" class="diff-line-num old_line">{{ line.old_line }}</td>
<td :class="line.type" class="diff-line-num new_line">{{ line.new_line }}</td>
- <td :class="line.type" class="line_content" v-html="trimChar(line.rich_text)"></td>
+ <td
+ :class="line.type"
+ class="line_content"
+ v-html="trimChar(line.rich_text) /* eslint-disable-line vue/no-v-html */"
+ ></td>
</tr>
</template>
<tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder">
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 55cf75132a9..831e6dd8f92 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -78,8 +78,8 @@ export default {
v-if="resolveAllDiscussionsIssuePath && !allResolved"
v-gl-tooltip
:href="resolveAllDiscussionsIssuePath"
- :title="s__('Resolve all threads in new issue')"
- :aria-label="s__('Resolve all threads in new issue')"
+ :title="s__('Create issue to resolve all threads')"
+ :aria-label="s__('Create issue to resolve all threads')"
class="new-issue-for-discussion discussion-create-issue-btn"
icon="issue-new"
/>
@@ -89,7 +89,7 @@ export default {
:title="__('Jump to next unresolved thread')"
:aria-label="__('Jump to next unresolved thread')"
class="discussion-next-btn"
- data-track-event="click_button"
+ data-track-action="click_button"
data-track-label="mr_next_unresolved_thread"
data-track-property="click_next_unresolved_thread_top"
icon="comment-next"
diff --git a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
index 9119d319d72..4ccba011014 100644
--- a/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
+++ b/app/assets/javascripts/notes/components/discussion_resolve_with_issue_button.vue
@@ -4,7 +4,7 @@ import { s__ } from '~/locale';
export default {
i18n: {
- buttonLabel: s__('MergeRequests|Resolve this thread in a new issue'),
+ buttonLabel: s__('MergeRequests|Create issue to resolve thread'),
},
name: 'ResolveWithIssueButton',
components: {
diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
index 0cd2afcf8a0..8c8cc7984b1 100644
--- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue
+++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue
@@ -19,7 +19,7 @@ export default {
<template>
<gl-button
v-gl-tooltip
- data-track-event="click_button"
+ data-track-action="click_button"
data-track-label="reply_comment_button"
category="tertiary"
icon="comment"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 9864e91c009..93f71276120 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
@@ -163,7 +162,11 @@ export default {
@addToBatch="addSuggestionToBatch"
@removeFromBatch="removeSuggestionFromBatch"
/>
- <div v-else class="note-text md" v-html="note.note_html"></div>
+ <div
+ v-else
+ class="note-text md"
+ v-html="note.note_html /* eslint-disable-line vue/no-v-html */"
+ ></div>
<note-form
v-if="isEditing"
ref="noteForm"
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index f2336e1b6f5..a4f06a8d9f5 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButton } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import { getDraft, updateDraft } from '~/lib/utils/autosave';
@@ -322,7 +321,7 @@ export default {
<div
v-if="conflictWhileEditing"
class="js-conflict-edit-warning alert alert-danger"
- v-html="changedCommentText"
+ v-html="changedCommentText /* eslint-disable-line vue/no-v-html */"
></div>
<div class="flash-container timeline-content"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 1a4a6c137a6..4e686ce8719 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -169,7 +168,7 @@ export default {
v-on="
authorStatusHasTooltip ? { mouseenter: removeEmojiTitle, mouseleave: addEmojiTitle } : {}
"
- v-html="authorStatus"
+ v-html="authorStatus /* eslint-disable-line vue/no-v-html */"
></span>
<span class="text-nowrap author-username">
<a
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 6a4a3263e4a..656591e0c32 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import $ from 'jquery';
import Visibility from 'visibilityjs';
import Vue from 'vue';
diff --git a/app/assets/javascripts/packages/details/components/package_history.vue b/app/assets/javascripts/packages/details/components/package_history.vue
index 0d7a73c12f1..27d2f208a42 100644
--- a/app/assets/javascripts/packages/details/components/package_history.vue
+++ b/app/assets/javascripts/packages/details/components/package_history.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { GlLink, GlSprintf } from '@gitlab/ui';
import { first } from 'lodash';
import { truncateSha } from '~/lib/utils/text_utility';
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index b4cdca34d92..f15c31b85c1 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { __, s__ } from '~/locale';
export const PackageType = {
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
index 4d6a1d5462b..74c0cb44c51 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
@@ -1,25 +1,24 @@
<script>
-import { GlLink, GlSprintf } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import Composer from '~/packages_and_registries/package_registry/components/details/metadata/composer.vue';
+import Conan from '~/packages_and_registries/package_registry/components/details/metadata/conan.vue';
+import Maven from '~/packages_and_registries/package_registry/components/details/metadata/maven.vue';
+import Nuget from '~/packages_and_registries/package_registry/components/details/metadata/nuget.vue';
+import Pypi from '~/packages_and_registries/package_registry/components/details/metadata/pypi.vue';
import {
- PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_COMPOSER,
PACKAGE_TYPE_CONAN,
PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_PYPI,
} from '~/packages_and_registries/package_registry/constants';
-import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
export default {
- i18n: {
- sourceText: s__('PackageRegistry|Source project located at %{link}'),
- licenseText: s__('PackageRegistry|License information located at %{link}'),
- recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
- appGroup: s__('PackageRegistry|App group: %{group}'),
- appName: s__('PackageRegistry|App name: %{name}'),
- },
components: {
- DetailsRow,
- GlLink,
- GlSprintf,
+ Composer,
+ Conan,
+ Maven,
+ Nuget,
+ Pypi,
},
props: {
packageEntity: {
@@ -28,21 +27,17 @@ export default {
},
},
computed: {
- showMetadata() {
- return (
- [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
- this.packageEntity.packageType,
- ) && this.packageEntity.metadata
- );
- },
- showNugetMetadata() {
- return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
+ metadataComponent() {
+ return {
+ [PACKAGE_TYPE_COMPOSER]: Composer,
+ [PACKAGE_TYPE_CONAN]: Conan,
+ [PACKAGE_TYPE_MAVEN]: Maven,
+ [PACKAGE_TYPE_NUGET]: Nuget,
+ [PACKAGE_TYPE_PYPI]: Pypi,
+ }[this.packageEntity.packageType];
},
- showConanMetadata() {
- return this.packageEntity.packageType === PACKAGE_TYPE_CONAN;
- },
- showMavenMetadata() {
- return this.packageEntity.packageType === PACKAGE_TYPE_MAVEN;
+ showMetadata() {
+ return this.metadataComponent && this.packageEntity.metadata;
},
},
};
@@ -51,56 +46,12 @@ export default {
<template>
<div v-if="showMetadata">
<h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3>
-
<div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
- <template v-if="showNugetMetadata">
- <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
- <gl-sprintf :message="$options.i18n.sourceText">
- <template #link>
- <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
- packageEntity.metadata.projectUrl
- }}</gl-link>
- </template>
- </gl-sprintf>
- </details-row>
- <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
- <gl-sprintf :message="$options.i18n.licenseText">
- <template #link>
- <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
- packageEntity.metadata.licenseUrl
- }}</gl-link>
- </template>
- </gl-sprintf>
- </details-row>
- </template>
-
- <details-row
- v-else-if="showConanMetadata"
- icon="information-o"
- padding="gl-p-4"
- data-testid="conan-recipe"
- >
- <gl-sprintf :message="$options.i18n.recipeText">
- <template #recipe>{{ packageEntity.metadata.recipe }}</template>
- </gl-sprintf>
- </details-row>
-
- <template v-else-if="showMavenMetadata">
- <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
- <gl-sprintf :message="$options.i18n.appName">
- <template #name>
- <strong>{{ packageEntity.metadata.appName }}</strong>
- </template>
- </gl-sprintf>
- </details-row>
- <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
- <gl-sprintf :message="$options.i18n.appGroup">
- <template #group>
- <strong>{{ packageEntity.metadata.appGroup }}</strong>
- </template>
- </gl-sprintf>
- </details-row>
- </template>
+ <component
+ :is="metadataComponent"
+ :package-entity="packageEntity"
+ data-testid="component-is"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue
new file mode 100644
index 00000000000..b6a36a0b00f
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/composer.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ i18n: {
+ targetShaCopyButton: s__('PackageRegistry|Copy target SHA'),
+ targetSha: s__('PackageRegistry|Target SHA: %{sha}'),
+ composerJson: s__(
+ 'PackageRegistry|Composer.json with license: %{license} and version: %{version}',
+ ),
+ },
+ components: {
+ DetailsRow,
+ GlSprintf,
+ ClipboardButton,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <details-row icon="information-o" padding="gl-p-4" dashed data-testid="composer-target-sha">
+ <gl-sprintf :message="$options.i18n.targetSha">
+ <template #sha>
+ <strong>{{ packageEntity.metadata.targetSha }}</strong>
+ <clipboard-button
+ :title="$options.i18n.targetShaCopyButton"
+ :text="packageEntity.metadata.targetSha"
+ category="tertiary"
+ css-class="gl-p-0!"
+ />
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="information-o" padding="gl-p-4" data-testid="composer-json">
+ <gl-sprintf :message="$options.i18n.composerJson">
+ <template #license>
+ <strong>{{ packageEntity.metadata.composerJson.license }}</strong>
+ </template>
+ <template #version>
+ <strong>{{ packageEntity.metadata.composerJson.version }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue
new file mode 100644
index 00000000000..10797d74acf
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/conan.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ i18n: {
+ recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
+ },
+ components: {
+ DetailsRow,
+ GlSprintf,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <details-row icon="information-o" padding="gl-p-4" data-testid="conan-recipe">
+ <gl-sprintf :message="$options.i18n.recipeText">
+ <template #recipe>{{ packageEntity.metadata.recipe }}</template>
+ </gl-sprintf>
+ </details-row>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue
new file mode 100644
index 00000000000..fd9fb49a9f2
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/maven.vue
@@ -0,0 +1,42 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ i18n: {
+ appGroup: s__('PackageRegistry|App group: %{group}'),
+ appName: s__('PackageRegistry|App name: %{name}'),
+ },
+ components: {
+ DetailsRow,
+ GlSprintf,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
+ <gl-sprintf :message="$options.i18n.appName">
+ <template #name>
+ <strong>{{ packageEntity.metadata.appName }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
+ <gl-sprintf :message="$options.i18n.appGroup">
+ <template #group>
+ <strong>{{ packageEntity.metadata.appGroup }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
new file mode 100644
index 00000000000..f0da7db6c91
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ i18n: {
+ sourceText: s__('PackageRegistry|Source project located at %{link}'),
+ licenseText: s__('PackageRegistry|License information located at %{link}'),
+ },
+ components: {
+ DetailsRow,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
+ <gl-sprintf :message="$options.i18n.sourceText">
+ <template #link>
+ <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
+ packageEntity.metadata.projectUrl
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
+ <gl-sprintf :message="$options.i18n.licenseText">
+ <template #link>
+ <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
+ packageEntity.metadata.licenseUrl
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
new file mode 100644
index 00000000000..6534eef532c
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+
+export default {
+ i18n: {
+ requiredPython: s__('PackageRegistry|Required Python: %{pythonVersion}'),
+ },
+ components: {
+ DetailsRow,
+ GlSprintf,
+ },
+ props: {
+ packageEntity: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <details-row icon="information-o" padding="gl-p-4" data-testid="pypi-required-python">
+ <gl-sprintf :message="$options.i18n.requiredPython">
+ <template #pythonVersion>
+ <strong>{{ packageEntity.metadata.requiredPython }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
index af4a984add4..408bd2e3dfe 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue
@@ -1,4 +1,5 @@
<script>
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { GlLink, GlSprintf } from '@gitlab/ui';
import { first } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
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 65547af3913..44d7807639d 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
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlSprintf, GlBadge } from '@gitlab/ui';
+import { GlIcon, GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
@@ -21,6 +21,9 @@ export default {
GlBadge,
TimeAgoTooltip,
},
+ directives: {
+ GlResizeObserver: GlResizeObserverDirective,
+ },
i18n: {
packageInfo: __('v%{version} published %{timeAgo}'),
},
@@ -60,18 +63,26 @@ export default {
},
},
mounted() {
- this.isDesktop = GlBreakpointInstance.isDesktop();
+ this.checkBreakpoints();
},
methods: {
dynamicSlotName(index) {
return `metadata-tag${index}`;
},
+ checkBreakpoints() {
+ this.isDesktop = GlBreakpointInstance.isDesktop();
+ },
},
};
</script>
<template>
- <title-area :title="packageEntity.name" :avatar="packageIcon" data-qa-selector="package_title">
+ <title-area
+ v-gl-resize-observer="checkBreakpoints"
+ :title="packageEntity.name"
+ :avatar="packageIcon"
+ data-qa-selector="package_title"
+ >
<template #sub-header>
<gl-icon name="eye" class="gl-mr-3" />
<span data-testid="sub-header">
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
new file mode 100644
index 00000000000..280d292ce0b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -0,0 +1,57 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import { s__ } from '~/locale';
+import { sortableFields } from '~/packages/list/utils';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+import PackageTypeToken from './tokens/package_type_token.vue';
+
+export default {
+ tokens: [
+ {
+ type: 'type',
+ icon: 'package',
+ title: s__('PackageRegistry|Type'),
+ unique: true,
+ token: PackageTypeToken,
+ operators: OPERATOR_IS_ONLY,
+ },
+ ],
+ components: { RegistrySearch, UrlSync },
+ computed: {
+ ...mapState({
+ isGroupPage: (state) => state.config.isGroupPage,
+ sorting: (state) => state.sorting,
+ filter: (state) => state.filter,
+ }),
+ sortableFields() {
+ return sortableFields(this.isGroupPage);
+ },
+ },
+ methods: {
+ ...mapActions(['setSorting', 'setFilter']),
+ updateSorting(newValue) {
+ this.setSorting(newValue);
+ this.$emit('update');
+ },
+ },
+};
+</script>
+
+<template>
+ <url-sync>
+ <template #default="{ updateQuery }">
+ <registry-search
+ :filter="filter"
+ :sorting="sorting"
+ :tokens="$options.tokens"
+ :sortable-fields="sortableFields"
+ @sorting:changed="updateSorting"
+ @filter:changed="setFilter"
+ @filter:submit="$emit('update')"
+ @query:changed="updateQuery"
+ />
+ </template>
+ </url-sync>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
new file mode 100644
index 00000000000..6e00a48586e
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue
@@ -0,0 +1,47 @@
+<script>
+import { n__ } from '~/locale';
+import { LIST_INTRO_TEXT, LIST_TITLE_TEXT } from '~/packages/list/constants';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+
+export default {
+ name: 'PackageTitle',
+ components: {
+ TitleArea,
+ MetadataItem,
+ },
+ props: {
+ count: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ helpUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ showPackageCount() {
+ return Number.isInteger(this.count);
+ },
+ packageAmountText() {
+ return n__(`%d Package`, `%d Packages`, this.count);
+ },
+ infoMessages() {
+ return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
+ },
+ },
+ i18n: {
+ LIST_TITLE_TEXT,
+ },
+};
+</script>
+
+<template>
+ <title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages">
+ <template #metadata-amount>
+ <metadata-item v-if="showPackageCount" icon="package" :text="packageAmountText" />
+ </template>
+ </title-area>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
new file mode 100644
index 00000000000..25bac687dbf
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -0,0 +1,129 @@
+<script>
+import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
+import { s__ } from '~/locale';
+import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import { TrackingActions } from '~/packages/shared/constants';
+import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import Tracking from '~/tracking';
+
+export default {
+ components: {
+ GlPagination,
+ GlModal,
+ GlSprintf,
+ PackagesListLoader,
+ PackagesListRow,
+ },
+ mixins: [Tracking.mixin()],
+ data() {
+ return {
+ itemToBeDeleted: null,
+ };
+ },
+ computed: {
+ ...mapState({
+ perPage: (state) => state.pagination.perPage,
+ totalItems: (state) => state.pagination.total,
+ page: (state) => state.pagination.page,
+ isGroupPage: (state) => state.config.isGroupPage,
+ isLoading: 'isLoading',
+ }),
+ ...mapGetters({ list: 'getList' }),
+ currentPage: {
+ get() {
+ return this.page;
+ },
+ set(value) {
+ this.$emit('page:changed', value);
+ },
+ },
+ isListEmpty() {
+ return !this.list || this.list.length === 0;
+ },
+ modalAction() {
+ return s__('PackageRegistry|Delete package');
+ },
+ deletePackageName() {
+ return this.itemToBeDeleted?.name ?? '';
+ },
+ tracking() {
+ const category = this.itemToBeDeleted
+ ? packageTypeToTrackCategory(this.itemToBeDeleted.package_type)
+ : undefined;
+ return {
+ category,
+ };
+ },
+ },
+ methods: {
+ setItemToBeDeleted(item) {
+ this.itemToBeDeleted = { ...item };
+ this.track(TrackingActions.REQUEST_DELETE_PACKAGE);
+ this.$refs.packageListDeleteModal.show();
+ },
+ deleteItemConfirmation() {
+ this.$emit('package:delete', this.itemToBeDeleted);
+ this.track(TrackingActions.DELETE_PACKAGE);
+ this.itemToBeDeleted = null;
+ },
+ deleteItemCanceled() {
+ this.track(TrackingActions.CANCEL_DELETE_PACKAGE);
+ this.itemToBeDeleted = null;
+ },
+ },
+ i18n: {
+ deleteModalContent: s__(
+ 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?',
+ ),
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <slot v-if="isListEmpty && !isLoading" name="empty-state"></slot>
+
+ <div v-else-if="isLoading">
+ <packages-list-loader />
+ </div>
+
+ <template v-else>
+ <div data-qa-selector="packages-table">
+ <packages-list-row
+ v-for="packageEntity in list"
+ :key="packageEntity.id"
+ :package-entity="packageEntity"
+ :package-link="packageEntity._links.web_path"
+ :is-group="isGroupPage"
+ @packageToDelete="setItemToBeDeleted"
+ />
+ </div>
+
+ <gl-pagination
+ v-model="currentPage"
+ :per-page="perPage"
+ :total-items="totalItems"
+ align="center"
+ class="gl-w-full gl-mt-3"
+ />
+
+ <gl-modal
+ ref="packageListDeleteModal"
+ modal-id="confirm-delete-pacakge"
+ ok-variant="danger"
+ @ok="deleteItemConfirmation"
+ @cancel="deleteItemCanceled"
+ >
+ <template #modal-title>{{ modalAction }}</template>
+ <template #modal-ok>{{ modalAction }}</template>
+ <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #name>
+ <strong>{{ deletePackageName }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue
new file mode 100644
index 00000000000..75fbdb80192
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue
@@ -0,0 +1,132 @@
+<script>
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { mapActions, mapState } from 'vuex';
+import createFlash from '~/flash';
+import { historyReplaceState } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
+import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
+import PackageList from './packages_list.vue';
+
+export default {
+ components: {
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ PackageList,
+ PackageTitle: () =>
+ import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'),
+ PackageSearch: () =>
+ import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'),
+ InfrastructureTitle: () =>
+ import(
+ /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'
+ ),
+ InfrastructureSearch: () =>
+ import(
+ /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'
+ ),
+ },
+ inject: {
+ titleComponent: {
+ from: 'titleComponent',
+ default: 'PackageTitle',
+ },
+ searchComponent: {
+ from: 'searchComponent',
+ default: 'PackageSearch',
+ },
+ emptyPageTitle: {
+ from: 'emptyPageTitle',
+ default: s__('PackageRegistry|There are no packages yet'),
+ },
+ noResultsText: {
+ from: 'noResultsText',
+ default: s__(
+ 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
+ ),
+ },
+ },
+ computed: {
+ ...mapState({
+ emptyListIllustration: (state) => state.config.emptyListIllustration,
+ emptyListHelpUrl: (state) => state.config.emptyListHelpUrl,
+ filter: (state) => state.filter,
+ selectedType: (state) => state.selectedType,
+ packageHelpUrl: (state) => state.config.packageHelpUrl,
+ packagesCount: (state) => state.pagination?.total,
+ }),
+ emptySearch() {
+ return (
+ this.filter.filter((f) => f.type !== FILTERED_SEARCH_TERM || f.value?.data).length === 0
+ );
+ },
+
+ emptyStateTitle() {
+ return this.emptySearch
+ ? this.emptyPageTitle
+ : s__('PackageRegistry|Sorry, your filter produced no results');
+ },
+ },
+ mounted() {
+ const queryParams = getQueryParams(window.document.location.search);
+ const { sorting, filters } = extractFilterAndSorting(queryParams);
+ this.setSorting(sorting);
+ this.setFilter(filters);
+ this.requestPackagesList();
+ this.checkDeleteAlert();
+ },
+ methods: {
+ ...mapActions([
+ 'requestPackagesList',
+ 'requestDeletePackage',
+ 'setSelectedType',
+ 'setSorting',
+ 'setFilter',
+ ]),
+ onPageChanged(page) {
+ return this.requestPackagesList({ page });
+ },
+ onPackageDeleteRequest(item) {
+ return this.requestDeletePackage(item);
+ },
+ checkDeleteAlert() {
+ const urlParams = new URLSearchParams(window.location.search);
+ 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' });
+ const cleanUrl = window.location.href.split('?')[0];
+ historyReplaceState(cleanUrl);
+ }
+ },
+ },
+ i18n: {
+ widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" />
+ <component :is="searchComponent" @update="requestPackagesList" />
+
+ <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
+ <template #empty-state>
+ <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
+ <template #description>
+ <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
+ <gl-sprintf v-else :message="noResultsText">
+ <template #noPackagesLink="{ content }">
+ <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+ </template>
+ </package-list>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
new file mode 100644
index 00000000000..529a7893dfc
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import { PACKAGE_TYPES } from '~/packages/list/constants';
+
+export default {
+ components: {
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ },
+ PACKAGE_TYPES,
+};
+</script>
+
+<template>
+ <gl-filtered-search-token v-bind="{ ...$attrs }" v-on="$listeners">
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="(type, index) in $options.PACKAGE_TYPES"
+ :key="index"
+ :value="type.type"
+ >
+ {{ type.title }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index aad888b4433..f023b4481a0 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -1,3 +1,4 @@
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { __, s__ } from '~/locale';
export const PACKAGE_TYPE_CONAN = 'CONAN';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
new file mode 100644
index 00000000000..1e01b75aabc
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import PackagesListApp from '../components/list/packages_list_app.vue';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-vue-packages-list');
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(PackagesListApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 6da2e3a47e8..bf286c84d5f 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -88,7 +88,7 @@ export default {
<template>
<section data-testid="registry-settings-app">
<cleanup-policy-enabled-alert v-if="showCleanupPolicyOnAlert" :project-path="projectPath" />
- <settings-block default-expanded>
+ <settings-block :collapsible="false">
<template #title> {{ __('Clean up image tags') }}</template>
<template #description>
<span data-testid="description">
diff --git a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
index d17c37e9e1a..99461475af0 100644
--- a/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
+++ b/app/assets/javascripts/pages/dashboard/projects/index/components/customize_homepage_banner.vue
@@ -77,7 +77,7 @@ export default {
);
if (button) {
- button.setAttribute('data-track-event', 'click_go_to_preferences');
+ button.setAttribute('data-track-action', 'click_go_to_preferences');
button.setAttribute('data-track-label', this.trackLabel);
}
},
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 342c054471d..8c9f23732aa 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,26 +1,30 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
-import { mountIssuablesListApp } from '~/issues_list';
+import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
-const ISSUE_BULK_UPDATE_PREFIX = 'issue_';
+if (gon.features?.vueIssuesList) {
+ mountIssuesListApp();
+} else {
+ const ISSUE_BULK_UPDATE_PREFIX = 'issue_';
-IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
-IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
-issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
+ IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
+ IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
+ issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
-initFilteredSearch({
- page: FILTERED_SEARCH.ISSUES,
- isGroupDecendent: true,
- useDefaultState: true,
- filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
-});
-projectSelect();
-initManualOrdering();
+ initFilteredSearch({
+ page: FILTERED_SEARCH.ISSUES,
+ isGroupDecendent: true,
+ useDefaultState: true,
+ filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
+ });
+ projectSelect();
+ initManualOrdering();
-if (gon.features?.vueIssuablesList) {
- mountIssuablesListApp();
+ if (gon.features?.vueIssuablesList) {
+ mountIssuablesListApp();
+ }
}
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 7557edb1b49..7b0418e1ad5 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -5,6 +5,7 @@ import Group from '~/group';
import { parseBoolean } from '~/lib/utils/common_utils';
import NewGroupCreationApp from './components/app.vue';
import GroupPathValidator from './group_path_validator';
+import initToggleInviteMembers from './toggle_invite_members';
new GroupPathValidator(); // eslint-disable-line no-new
@@ -31,3 +32,5 @@ function initNewGroupCreation(el) {
const el = document.querySelector('.js-new-group-creation');
initNewGroupCreation(el);
+
+initToggleInviteMembers();
diff --git a/app/assets/javascripts/pages/groups/new/toggle_invite_members.js b/app/assets/javascripts/pages/groups/new/toggle_invite_members.js
new file mode 100644
index 00000000000..ffb4964cf7d
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/new/toggle_invite_members.js
@@ -0,0 +1,14 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+
+export default function initToggleInviteMembers() {
+ const inviteMembersSection = document.querySelector('.js-invite-members-section');
+ const setupForCompanyRadios = document.querySelectorAll('input[name="group[setup_for_company]"]');
+
+ if (inviteMembersSection && setupForCompanyRadios.length) {
+ setupForCompanyRadios.forEach((el) => {
+ el.addEventListener('change', (event) => {
+ inviteMembersSection.classList.toggle('hidden', !parseBoolean(event.target.value));
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index b365e039191..80bcbefab46 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -14,7 +14,7 @@ import '~/sourcegraph/load';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
const viewBlobEl = document.querySelector('#js-view-blob-app');
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index e3b30560fef..c6a76df7bde 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -4,8 +4,8 @@ import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import createFlash from '~/flash';
-import initChangesDropdown from '~/init_changes_dropdown';
-import initNotes from '~/init_notes';
+import initDeprecatedNotes from '~/init_deprecated_notes';
+import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -17,13 +17,13 @@ import '~/sourcegraph/load';
const hasPerfBar = document.querySelector('.with-performance-bar');
const performanceHeight = hasPerfBar ? 35 : 0;
-initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
+initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
new ZenMode();
new ShortcutsNavigation();
initCommitBoxInfo();
-initNotes();
+initDeprecatedNotes();
const filesContainer = $('.js-diffs-batch');
diff --git a/app/assets/javascripts/pages/projects/compare/show/index.js b/app/assets/javascripts/pages/projects/compare/show/index.js
index 5edaa7f7e51..b74f7d1cf57 100644
--- a/app/assets/javascripts/pages/projects/compare/show/index.js
+++ b/app/assets/javascripts/pages/projects/compare/show/index.js
@@ -1,11 +1,11 @@
import Diff from '~/diff';
import GpgBadges from '~/gpg_badges';
-import initChangesDropdown from '~/init_changes_dropdown';
+import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import initCompareSelector from '~/projects/compare';
initCompareSelector();
new Diff(); // eslint-disable-line no-new
const paddingTop = 16;
-initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
+initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight - paddingTop);
GpgBadges.fetch();
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index e365f51567d..62aa5df888f 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -6,7 +6,7 @@ import Issue from '~/issue';
import initIncidentApp from '~/issue_show/incident';
import { initIssuableApp, initIssueHeaderActions } from '~/issue_show/issue';
import { parseIssuableData } from '~/issue_show/utils/parse_data';
-import initNotesApp from '~/notes/index';
+import initNotesApp from '~/notes';
import { store } from '~/notes/stores';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index e4f99d1e7fd..1282d2aa303 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,7 +1,8 @@
+import { store } from '~/notes/stores';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initShow from '../show';
initShow();
-initSidebarBundle();
+initSidebarBundle(store);
initRelatedIssues();
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
index 51980b2d971..51980b2d971 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
deleted file mode 100644
index 8f92ce95dbf..00000000000
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<script>
-import { GlProgressBar, GlSprintf } from '@gitlab/ui';
-import { pick } from 'lodash';
-import { s__ } from '~/locale';
-import { ACTION_LABELS } from '../constants';
-import LearnGitlabInfoCard from './learn_gitlab_info_card.vue';
-
-export default {
- components: { LearnGitlabInfoCard, GlProgressBar, GlSprintf },
- i18n: {
- title: s__('LearnGitLab|Learn GitLab'),
- description: s__(
- 'LearnGitLab|Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.',
- ),
- percentageCompleted: s__(`LearnGitLab|%{percentage}%{percentSymbol} completed`),
- workspace: {
- title: s__('LearnGitLab|Set up your workspace'),
- description: s__(
- "LearnGitLab|Complete these tasks first so you can enjoy GitLab's features to their fullest:",
- ),
- },
- plan: {
- title: s__('LearnGitLab|Plan and execute'),
- description: s__(
- 'LearnGitLab|Create a workflow for your new workspace, and learn how GitLab features work together:',
- ),
- },
- deploy: {
- title: s__('LearnGitLab|Deploy'),
- description: s__(
- 'LearnGitLab|Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:',
- ),
- },
- },
- props: {
- actions: {
- required: true,
- type: Object,
- },
- },
- maxValue: Object.keys(ACTION_LABELS).length,
- methods: {
- infoProps(action) {
- return {
- ...this.actions[action],
- ...pick(ACTION_LABELS[action], ['title', 'actionLabel', 'description', 'trialRequired']),
- };
- },
- progressValue() {
- return Object.values(this.actions).filter((a) => a.completed).length;
- },
- progressPercentage() {
- return Math.round((this.progressValue() / this.$options.maxValue) * 100);
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="row">
- <div class="gl-mb-7 col-md-8 col-lg-7">
- <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
- <p class="gl-text-gray-700 gl-mb-0">{{ $options.i18n.description }}</p>
- </div>
- </div>
-
- <div class="gl-mb-3">
- <p class="gl-text-gray-500 gl-mb-2" data-testid="completion-percentage">
- <gl-sprintf :message="$options.i18n.percentageCompleted">
- <template #percentage>{{ progressPercentage() }}</template>
- <template #percentSymbol>%</template>
- </gl-sprintf>
- </p>
- <gl-progress-bar :value="progressValue()" :max="$options.maxValue" />
- </div>
-
- <h2 class="gl-font-lg gl-mb-3">{{ $options.i18n.workspace.title }}</h2>
- <p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n.workspace.description }}</p>
-
- <div class="row row-cols-2 row-cols-md-3 row-cols-lg-4">
- <div class="col gl-mb-6"><learn-gitlab-info-card v-bind="infoProps('userAdded')" /></div>
- <div class="col gl-mb-6"><learn-gitlab-info-card v-bind="infoProps('gitWrite')" /></div>
- <div class="col gl-mb-6">
- <learn-gitlab-info-card v-bind="infoProps('pipelineCreated')" />
- </div>
- <div class="col gl-mb-6"><learn-gitlab-info-card v-bind="infoProps('trialStarted')" /></div>
- <div class="col gl-mb-6">
- <learn-gitlab-info-card v-bind="infoProps('codeOwnersEnabled')" />
- </div>
- <div class="col gl-mb-6">
- <learn-gitlab-info-card v-bind="infoProps('requiredMrApprovalsEnabled')" />
- </div>
- </div>
-
- <h2 class="gl-font-lg gl-mb-3">{{ $options.i18n.plan.title }}</h2>
- <p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n.plan.description }}</p>
-
- <div class="row row-cols-2 row-cols-md-3 row-cols-lg-4">
- <div class="col gl-mb-6">
- <learn-gitlab-info-card v-bind="infoProps('issueCreated')" />
- </div>
- <div class="col gl-mb-6">
- <learn-gitlab-info-card v-bind="infoProps('mergeRequestCreated')" />
- </div>
- </div>
-
- <h2 class="gl-font-lg gl-mb-3">{{ $options.i18n.deploy.title }}</h2>
- <p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n.deploy.description }}</p>
-
- <div class="row row-cols-2 row-cols-lg-4 g-2 g-lg-3">
- <div class="col gl-mb-6">
- <learn-gitlab-info-card v-bind="infoProps('securityScanEnabled')" />
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
index 3d31ac6c267..69fb5878f5c 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -39,7 +39,7 @@ export default {
:href="value.url"
data-track-action="click_link"
:data-track-label="$options.i18n.ACTION_LABELS[action].title"
- data-track-property="Growth::Conversion::Experiment::LearnGitLabA"
+ data-track-property="Growth::Conversion::Experiment::LearnGitLab"
>
{{ $options.i18n.ACTION_LABELS[action].title }}
</gl-link>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
index ac7c94bdd9e..6da0a8fd212 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js
@@ -1,8 +1,6 @@
import Vue from 'vue';
-import trackLearnGitlab from '~/learn_gitlab/track_learn_gitlab';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import LearnGitlabA from '../components/learn_gitlab_a.vue';
-import LearnGitlabB from '../components/learn_gitlab_b.vue';
+import LearnGitlab from '../components/learn_gitlab.vue';
function initLearnGitlab() {
const el = document.getElementById('js-learn-gitlab-app');
@@ -14,14 +12,10 @@ function initLearnGitlab() {
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
- const { learnGitlabA } = gon.experiments;
-
- trackLearnGitlab(learnGitlabA);
-
return new Vue({
el,
render(createElement) {
- return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, {
+ return createElement(LearnGitlab, {
props: { actions, sections },
});
},
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index d6b6c9fe06a..dadf0988582 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -2,11 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import { initPipelineCountListener } from '~/commit/pipelines/utils';
import initIssuableSidebar from '~/init_issuable_sidebar';
import StatusBox from '~/issuable/components/status_box.vue';
import createDefaultClient from '~/lib/graphql';
-import { handleLocationHash } from '~/lib/utils/common_utils';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
import getStateQuery from './queries/get_state.query.graphql';
@@ -15,11 +14,10 @@ export default function initMergeRequestShow() {
const awardEmojiEl = document.getElementById('js-vue-awards-block');
new ZenMode(); // eslint-disable-line no-new
- initIssuableSidebar();
- initPipelines();
+ initPipelineCountListener(document.querySelector('#commit-pipeline-table-view'));
new ShortcutsIssuable(true); // eslint-disable-line no-new
- handleLocationHash();
initSourcegraph();
+ initIssuableSidebar();
if (awardEmojiEl) {
import('~/emoji/awards_app')
.then((m) => m.default(awardEmojiEl))
@@ -29,7 +27,10 @@ export default function initMergeRequestShow() {
}
const el = document.querySelector('.js-mr-status-box');
- const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() });
+ const apolloProvider = new VueApollo({
+ assumeImmutableResults: true,
+ defaultClient: createDefaultClient(),
+ });
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 546fa66eda6..25dede33880 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -5,8 +5,11 @@ import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
import initShow from '../init_merge_request_show';
-initShow();
-initSidebarBundle();
initMrNotes();
-initReviewBar();
-initIssuableHeaderWarning(store);
+initShow();
+
+requestIdleCallback(() => {
+ initSidebarBundle(store);
+ initReviewBar();
+ initIssuableHeaderWarning(store);
+});
diff --git a/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
new file mode 100644
index 00000000000..ba8858c985a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/new/components/new_project_url_select.vue
@@ -0,0 +1,98 @@
+<script>
+import {
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import Tracking from '~/tracking';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql';
+
+export default {
+ components: {
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownSectionHeader,
+ GlLoadingIcon,
+ GlSearchBoxByType,
+ },
+ mixins: [Tracking.mixin()],
+ apollo: {
+ currentUser: {
+ query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ variables() {
+ return {
+ search: this.search,
+ };
+ },
+ skip() {
+ return this.search.length > 0 && this.search.length < MINIMUM_SEARCH_LENGTH;
+ },
+ debounce: DEBOUNCE_DELAY,
+ },
+ },
+ inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel'],
+ data() {
+ return {
+ currentUser: {},
+ search: '',
+ selectedNamespace: {
+ id: this.namespaceId,
+ fullPath: this.namespaceFullPath,
+ },
+ };
+ },
+ computed: {
+ userGroups() {
+ return this.currentUser.groups?.nodes || [];
+ },
+ userNamespace() {
+ return this.currentUser.namespace || {};
+ },
+ },
+ methods: {
+ handleClick({ id, fullPath }) {
+ this.selectedNamespace = {
+ id: getIdFromGraphQLId(id),
+ fullPath,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button-group class="gl-w-full">
+ <gl-button label>{{ rootUrl }}</gl-button>
+ <gl-dropdown
+ class="gl-w-full"
+ :text="selectedNamespace.fullPath"
+ toggle-class="gl-rounded-top-right-base! gl-rounded-bottom-right-base!"
+ data-qa-selector="select_namespace_dropdown"
+ @show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
+ >
+ <gl-search-box-by-type v-model.trim="search" />
+ <gl-loading-icon v-if="$apollo.queries.currentUser.loading" />
+ <template v-else>
+ <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
+ <gl-dropdown-item v-for="group of userGroups" :key="group.id" @click="handleClick(group)">
+ {{ group.fullPath }}
+ </gl-dropdown-item>
+ <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
+ <gl-dropdown-item @click="handleClick(userNamespace)">
+ {{ userNamespace.fullPath }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+
+ <input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index f469c56e808..ed816e3be95 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
import NewProjectCreationApp from './components/app.vue';
+import NewProjectUrlSelect from './components/new_project_url_select.vue';
-initProjectVisibilitySelector();
-initProjectNew.bindEvents();
+function initNewProjectCreation() {
+ const el = document.querySelector('.js-new-project-creation');
-function initNewProjectCreation(el) {
const {
pushToCreateProjectCommand,
workingWithProjectsHelpPath,
@@ -29,9 +31,6 @@ function initNewProjectCreation(el) {
return new Vue({
el,
- components: {
- NewProjectCreationApp,
- },
provide,
render(h) {
return h(NewProjectCreationApp, { props });
@@ -39,6 +38,31 @@ function initNewProjectCreation(el) {
});
}
-const el = document.querySelector('.js-new-project-creation');
+function initNewProjectUrlSelect() {
+ const el = document.querySelector('.js-vue-new-project-url-select');
+
+ if (!el) {
+ return undefined;
+ }
-initNewProjectCreation(el);
+ Vue.use(VueApollo);
+
+ return new Vue({
+ el,
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ }),
+ provide: {
+ namespaceFullPath: el.dataset.namespaceFullPath,
+ namespaceId: el.dataset.namespaceId,
+ rootUrl: el.dataset.rootUrl,
+ trackLabel: el.dataset.trackLabel,
+ },
+ render: (createElement) => createElement(NewProjectUrlSelect),
+ });
+}
+
+initProjectVisibilitySelector();
+initProjectNew.bindEvents();
+initNewProjectCreation();
+initNewProjectUrlSelect();
diff --git a/app/assets/javascripts/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
new file mode 100644
index 00000000000..e16fe5dde49
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql
@@ -0,0 +1,14 @@
+query searchNamespacesWhereUserCanCreateProjects($search: String) {
+ currentUser {
+ groups(permissionScope: CREATE_PROJECTS, search: $search) {
+ nodes {
+ id
+ fullPath
+ }
+ }
+ namespace {
+ id
+ fullPath
+ }
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/packages/packages/show/index.js b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
index ee06f247ddc..2dee87985cb 100644
--- a/app/assets/javascripts/pages/projects/packages/packages/show/index.js
+++ b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
@@ -1,11 +1,3 @@
-(async function initPackage() {
- let app;
- if (document.getElementById('js-vue-packages-detail-new')) {
- app = await import(
- /* webpackChunkName: 'new_package_app' */ `~/packages_and_registries/package_registry/pages/details.js`
- );
- } else {
- app = await import('~/packages/details/');
- }
- app.default();
-})();
+import initPackageDetails from '~/packages_and_registries/package_registry/pages/details';
+
+initPackageDetails();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index d0ec5668d21..0e646e8c505 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -141,9 +141,7 @@ export default {
return Math.floor(Math.random() * 28);
},
showDailyLimitMessage({ value }) {
- return (
- value === KEY_CUSTOM && this.glFeatures.ciDailyLimitForPipelineSchedules && this.dailyLimit
- );
+ return value === KEY_CUSTOM && this.dailyLimit;
},
},
};
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 92b2bc9644b..42b08bcaa7b 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -53,7 +53,7 @@ Those scheduled pipelines will inherit limited project access based on their ass
<p>
{{ __('Learn more in the') }}
<a :href="docsUrl" target="_blank" rel="nofollow">
- {{ s__('Learn more in the|pipeline schedules documentation') }}</a
+ {{ __('pipeline schedules documentation') }}</a
>.
<!-- oneline to prevent extra space before period -->
</p>
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index fb0be31834d..0b662c945c6 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -1,4 +1,5 @@
import groupsSelect from '~/groups_select';
+import initImportAProjectModal from '~/invite_members/init_import_a_project_modal';
import initInviteGroupTrigger from '~/invite_members/init_invite_group_trigger';
import initInviteMembersForm from '~/invite_members/init_invite_members_form';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
@@ -14,6 +15,7 @@ import UsersSelect from '~/users_select';
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
+initImportAProjectModal();
initInviteMembersModal();
initInviteMembersTrigger();
initInviteGroupTrigger();
diff --git a/app/assets/javascripts/pages/projects/usage_quotas/index.js b/app/assets/javascripts/pages/projects/usage_quotas/index.js
new file mode 100644
index 00000000000..9cd80b85c8a
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/usage_quotas/index.js
@@ -0,0 +1,23 @@
+import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
+import storageCounter from '~/projects/storage_counter';
+import initSearchSettings from '~/search_settings';
+
+const initLinkedTabs = () => {
+ if (!document.querySelector('.js-usage-quota-tabs')) {
+ return false;
+ }
+
+ return new LinkedTabs({
+ defaultAction: '#storage-quota-tab',
+ parentEl: '.js-usage-quota-tabs',
+ hashedTabs: true,
+ });
+};
+
+const initVueApp = () => {
+ storageCounter('js-project-storage-count-app');
+};
+
+initVueApp();
+initLinkedTabs();
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index dead61cf358..2c1f9e634ab 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -1,3 +1,5 @@
+import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import initWikis from '~/pages/shared/wikis';
initWikis();
+initDiffStatsDropdown();
diff --git a/app/assets/javascripts/pages/projects/work_items/index/index.js b/app/assets/javascripts/pages/projects/work_items/index/index.js
new file mode 100644
index 00000000000..11c257611f0
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/work_items/index/index.js
@@ -0,0 +1,3 @@
+import { initWorkItemsRoot } from '~/work_items/index';
+
+initWorkItemsRoot();
diff --git a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
index 1e7c29aefaa..7e646125331 100644
--- a/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
+++ b/app/assets/javascripts/pages/sessions/new/signin_tabs_memoizer.js
@@ -8,7 +8,7 @@ export default class SigninTabsMemoizer {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.new-session-tabs' } = {}) {
this.currentTabKey = currentTabKey;
this.tabSelector = tabSelector;
- this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
// sets selected tab if given as hash tag
if (window.location.hash) {
this.saveData(window.location.hash);
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index 670b0535ca3..f204f0ebfaa 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
+import { GlSafeHtmlDirective } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
import { s__ } from '~/locale';
@@ -13,6 +13,9 @@ export default {
DetailedMetric,
RequestSelector,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
store: {
type: Object,
@@ -129,6 +132,7 @@ export default {
this.currentRequest = newRequestId;
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
@@ -144,7 +148,7 @@ export default {
class="current-host"
:class="{ canary: currentRequest.details.host.canary }"
>
- <span v-html="birdEmoji"></span>
+ <span v-safe-html:[$options.safeHtmlConfig]="birdEmoji"></span>
{{ currentRequest.details.host.hostname }}
</span>
</div>
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index 75fb7bbc5c5..a46ac620f48 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlPopover } from '@gitlab/ui';
+import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
import { n__ } from '~/locale';
@@ -8,6 +7,9 @@ export default {
components: {
GlPopover,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
currentRequest: {
type: Object,
@@ -43,6 +45,7 @@ export default {
methods: {
glEmojiTag,
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
@@ -59,7 +62,10 @@ export default {
</option>
</select>
<span v-if="requestsWithWarnings.length" class="gl-cursor-default">
- <span id="performance-bar-request-selector-warning" v-html="glEmojiTag('warning')"></span>
+ <span
+ id="performance-bar-request-selector-warning"
+ v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('warning')"
+ ></span>
<gl-popover
placement="bottom"
target="performance-bar-request-selector-warning"
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 7fe6b088ebb..3ebd222029b 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,12 +1,14 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlPopover } from '@gitlab/ui';
+import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
import { glEmojiTag } from '~/emoji';
export default {
components: {
GlPopover,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
htmlId: {
type: String,
@@ -32,11 +34,12 @@ export default {
methods: {
glEmojiTag,
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
<template>
<span v-if="hasWarnings" class="gl-cursor-default">
- <span :id="htmlId" v-html="glEmojiTag('warning')"></span>
+ <span :id="htmlId" v-safe-html:[$options.safeHtmlConfig]="glEmojiTag('warning')"></span>
<gl-popover placement="bottom" :target="htmlId" :content="warningMessage" />
</span>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
index 8f4894a0bde..0308cd9c565 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
@@ -10,7 +10,6 @@ import {
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
-import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
@@ -37,6 +36,11 @@ export default {
type: String,
required: true,
},
+ commitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -49,9 +53,6 @@ export default {
isNewCiConfigFile: {
query: getIsNewCiConfigFile,
},
- commitSha: {
- query: getCommitSha,
- },
currentBranch: {
query: getCurrentBranch,
},
@@ -96,13 +97,7 @@ export default {
lastCommitId: this.commitSha,
},
update(store, { data }) {
- const commitSha = data?.commitCreate?.commit?.sha;
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
-
- if (commitSha) {
- store.writeQuery({ query: getCommitSha, data: { commitSha } });
- }
-
if (pipelineEtag) {
store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
}
@@ -117,6 +112,9 @@ export default {
this.$emit('commit', { type: COMMIT_SUCCESS });
this.updateLastCommitBranch(targetBranch);
this.updateCurrentBranch(targetBranch);
+ if (this.currentBranch === targetBranch) {
+ this.$emit('updateCommitSha');
+ }
}
} catch (error) {
this.$emit('showError', { type: COMMIT_FAILURE, reasons: [error?.message] });
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
index 77ede396496..f2a0f474bc4 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
@@ -3,7 +3,6 @@ import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
export default {
components: {
@@ -12,14 +11,11 @@ export default {
mixins: [glFeatureFlagMixin()],
inject: ['ciConfigPath', 'projectPath', 'projectNamespace', 'defaultBranch'],
inheritAttrs: false,
- data() {
- return {
- commitSha: '',
- };
- },
- apollo: {
+ props: {
commitSha: {
- query: getCommitSha,
+ type: String,
+ required: false,
+ default: '',
},
},
methods: {
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index 9a6eed50fbe..68065cc3c73 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -158,11 +158,9 @@ export default {
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
- this.$emit('updateCommitSha', { newBranch });
-
// refetching the content will cause a lot of components to re-render,
// including the text editor which uses the commit sha to register the CI schema
- // so we need to make sure the commit sha is updated first
+ // so we need to make sure the currentBranch (and consequently, the commitSha) are updated first
await this.$nextTick();
this.$emit('refetchContent');
},
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
index ebe73bdcec3..551a0430fbf 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue
@@ -1,21 +1,14 @@
<script>
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import BranchSwitcher from './branch_switcher.vue';
export default {
components: {
BranchSwitcher,
},
- mixins: [glFeatureFlagsMixin()],
- computed: {
- showBranchSwitcher() {
- return this.glFeatures.pipelineEditorBranchSwitcher;
- },
- },
};
</script>
<template>
<div class="gl-mb-4">
- <branch-switcher v-if="showBranchSwitcher" v-on="$listeners" />
+ <branch-switcher v-on="$listeners" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
index 24bca04e115..fcc31f087ff 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
@@ -33,6 +33,11 @@ export default {
type: Object,
required: true,
},
+ commitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
isNewCiConfigFile: {
type: Boolean,
required: true,
@@ -54,7 +59,11 @@ export default {
</script>
<template>
<div class="gl-mb-5">
- <pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
+ <pipeline-status
+ v-if="showPipelineStatus"
+ :commit-sha="commitSha"
+ :class="$options.pipelineStatusClasses"
+ />
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index 46f6f4a28c1..ec240854be5 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -3,7 +3,6 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
-import getCommitSha from '~/pipeline_editor/graphql/queries/client/commit_sha.graphql';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.graphql';
import {
@@ -33,10 +32,14 @@ export default {
GlSprintf,
},
inject: ['projectFullPath'],
- apollo: {
+ props: {
commitSha: {
- query: getCommitSha,
+ type: String,
+ required: false,
+ default: '',
},
+ },
+ apollo: {
pipelineEtag: {
query: getPipelineEtag,
},
@@ -51,7 +54,7 @@ export default {
sha: this.commitSha,
};
},
- update: (data) => {
+ update(data) {
const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
return {
@@ -60,6 +63,11 @@ export default {
detailedStatus,
};
},
+ result(res) {
+ if (res.data?.project?.pipeline) {
+ this.hasError = false;
+ }
+ },
error() {
this.hasError = true;
},
@@ -68,7 +76,6 @@ export default {
},
data() {
return {
- commitSha: '',
hasError: false,
};
},
@@ -84,7 +91,11 @@ export default {
// (e.g. pipeline is null during fetch when the pipeline hasn't been
// triggered yet), we can just show the loading state until the pipeline
// details are ready to be fetched
- return this.$apollo.queries.pipeline.loading || (!this.hasPipelineData && !this.hasError);
+ return (
+ this.$apollo.queries.pipeline.loading ||
+ this.commitSha.length === 0 ||
+ (!this.hasPipelineData && !this.hasError)
+ );
},
shortSha() {
return truncateSha(this.commitSha);
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index e463fcf379d..f7c9f10ea46 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -69,6 +69,11 @@ export default {
type: String,
required: true,
},
+ commitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
apollo: {
appStatus: {
@@ -110,7 +115,7 @@ export default {
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
<ci-editor-header />
- <text-editor :value="ciFileContent" v-on="$listeners" />
+ <text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<editor-tab
class="gl-mb-3"
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 0ac4a40ff4a..fbb66231f16 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
@@ -24,9 +24,6 @@ export default {
},
},
computed: {
- showFileNav() {
- return this.glFeatures.pipelineEditorBranchSwitcher;
- },
showCTAButton() {
return this.glFeatures.pipelineEditorEmptyStateAction;
},
@@ -40,7 +37,7 @@ export default {
</script>
<template>
<div>
- <pipeline-editor-file-nav v-if="showFileNav" v-on="$listeners" />
+ <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>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index d05b06d16db..bb03fa126a5 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -43,3 +43,5 @@ export const pipelineEditorTrackingOptions = {
export const TEMPLATE_REPOSITORY_URL =
'https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates';
+
+export const COMMIT_SHA_POLL_INTERVAL = 1000;
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql
deleted file mode 100644
index dce17cad808..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-mutation updateCommitSha($commitSha: String) {
- updateCommitSha(commitSha: $commitSha) @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
deleted file mode 100644
index 6c7635887ec..00000000000
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/commit_sha.graphql
+++ /dev/null
@@ -1,3 +0,0 @@
-query getCommitSha {
- commitSha @client
-}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
index 219c23bb22b..02d49507947 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
@@ -1,11 +1,10 @@
query getLatestCommitSha($projectPath: ID!, $ref: String) {
project(fullPath: $projectPath) {
- pipelines(ref: $ref) {
- nodes {
- id
- sha
- path
- commitPath
+ repository {
+ tree(ref: $ref) {
+ lastCommit {
+ sha
+ }
}
}
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index 2bec2006e95..a34652b1495 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,6 +1,5 @@
import produce from 'immer';
import axios from '~/lib/utils/axios_utils';
-import getCommitShaQuery from './queries/client/commit_sha.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
@@ -32,14 +31,6 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
- updateCommitSha: (_, { commitSha }, { cache }) => {
- cache.writeQuery({
- query: getCommitShaQuery,
- data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
- draftData.commitSha = commitSha;
- }),
- });
- },
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index e0f8d889cad..89b9091e6f9 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -4,7 +4,6 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
-import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
@@ -26,7 +25,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const {
// Add to apollo cache as it can be updated by future queries
- commitSha,
initialBranchName,
pipelineEtag,
// Add to provide/inject API for static values
@@ -58,7 +56,11 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, { typeDefs, useGet: true }),
+ defaultClient: createDefaultClient(resolvers, {
+ typeDefs,
+ useGet: true,
+ assumeImmutableResults: true,
+ }),
});
const { cache } = apolloProvider.clients.defaultClient;
@@ -70,13 +72,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
});
cache.writeQuery({
- query: getCommitSha,
- data: {
- commitSha,
- },
- });
-
- cache.writeQuery({
query: getPipelineEtag,
data: {
pipelineEtag,
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index 0e8a6805a59..e70417145ab 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -10,17 +10,16 @@ import ConfirmUnsavedChangesDialog from './components/ui/confirm_unsaved_changes
import PipelineEditorEmptyState from './components/ui/pipeline_editor_empty_state.vue';
import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue';
import {
+ COMMIT_SHA_POLL_INTERVAL,
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_ERROR,
EDITOR_APP_STATUS_LOADING,
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
-import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
-import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
@@ -50,6 +49,7 @@ export default {
failureType: null,
failureReasons: [],
initialCiFileContent: '',
+ isFetchingCommitSha: false,
isNewCiConfigFile: false,
lastCommittedContent: '',
currentCiFileContent: '',
@@ -136,7 +136,7 @@ export default {
update(data) {
const { ciConfig } = data || {};
const stageNodes = ciConfig?.stages?.nodes || [];
- const stages = unwrapStagesWithNeeds(stageNodes);
+ const stages = unwrapStagesWithNeeds(JSON.parse(JSON.stringify(stageNodes)));
return { ...ciConfig, stages };
},
@@ -156,7 +156,25 @@ export default {
query: getAppStatus,
},
commitSha: {
- query: getCommitSha,
+ query: getLatestCommitShaQuery,
+ variables() {
+ return {
+ projectPath: this.projectFullPath,
+ ref: this.currentBranch,
+ };
+ },
+ update(data) {
+ const latestCommitSha = data.project?.repository?.tree?.lastCommit?.sha;
+
+ if (this.isFetchingCommitSha && latestCommitSha === this.commitSha) {
+ this.$apollo.queries.commitSha.startPolling(COMMIT_SHA_POLL_INTERVAL);
+ return this.commitSha;
+ }
+
+ this.isFetchingCommitSha = false;
+ this.$apollo.queries.commitSha.stopPolling();
+ return latestCommitSha;
+ },
},
currentBranch: {
query: getCurrentBranch,
@@ -257,37 +275,9 @@ export default {
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
- async updateCommitSha({ newBranch }) {
- let fetchResults;
-
- try {
- fetchResults = await this.$apollo.query({
- query: getLatestCommitShaQuery,
- variables: {
- projectPath: this.projectFullPath,
- ref: newBranch,
- },
- });
- } catch {
- this.showFetchError();
- return;
- }
-
- if (fetchResults.errors?.length > 0) {
- this.showFetchError();
- return;
- }
-
- const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
- if (pipelineNodes.length === 0) {
- return;
- }
-
- const commitSha = pipelineNodes[0].sha;
- this.$apollo.mutate({
- mutation: updateCommitShaMutation,
- variables: { commitSha },
- });
+ updateCommitSha() {
+ this.isFetchingCommitSha = true;
+ this.$apollo.queries.commitSha.refetch();
},
updateOnCommit({ type }) {
this.reportSuccess(type);
@@ -336,6 +326,7 @@ export default {
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
:is-new-ci-config-file="isNewCiConfigFile"
+ :commit-sha="commitSha"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index dfe9c82b912..4324c64ab3b 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -25,6 +25,11 @@ export default {
type: String,
required: true,
},
+ commitSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
isNewCiConfigFile: {
type: Boolean,
required: true,
@@ -56,15 +61,22 @@ export default {
<pipeline-editor-file-nav v-on="$listeners" />
<pipeline-editor-header
:ci-config-data="ciConfigData"
+ :commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
+ :commit-sha="commitSha"
v-on="$listeners"
@set-current-tab="setCurrentTab"
/>
- <commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
+ <commit-section
+ v-if="showCommitForm"
+ :ci-file-content="ciFileContent"
+ :commit-sha="commitSha"
+ v-on="$listeners"
+ />
<pipeline-editor-drawer v-if="showPipelineDrawer" />
</div>
</template>
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 5472e51445a..d74b6e8edf6 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -123,6 +123,7 @@ export default {
isWarningDismissed: false,
isLoading: false,
submitted: false,
+ ccAlertDismissed: false,
};
},
computed: {
@@ -151,7 +152,7 @@ export default {
return this.form[this.refFullName]?.descriptions ?? {};
},
ccRequiredError() {
- return this.error === CC_VALIDATION_REQUIRED_ERROR;
+ return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
},
},
watch: {
@@ -292,6 +293,7 @@ export default {
},
createPipeline() {
this.submitted = true;
+ this.ccAlertDismissed = false;
return axios
.post(this.pipelinesPath, {
@@ -333,13 +335,17 @@ export default {
this.warnings = warnings;
this.totalWarnings = totalWarnings;
},
+ dismissError() {
+ this.ccAlertDismissed = true;
+ this.error = null;
+ },
},
};
</script>
<template>
<gl-form @submit.prevent="createPipeline">
- <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" />
+ <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" />
<gl-alert
v-else-if="error"
:title="errorTitle"
diff --git a/app/assets/javascripts/pipelines/components/graph/accessors.js b/app/assets/javascripts/pipelines/components/graph/accessors.js
deleted file mode 100644
index 6ece855bcd8..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/accessors.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { get } from 'lodash';
-import { REST, GRAPHQL } from './constants';
-
-const accessors = {
- [REST]: {
- detailsPath: 'details_path',
- groupId: 'id',
- hasDetails: 'has_details',
- pipelineStatus: ['details', 'status'],
- sourceJob: ['source_job', 'name'],
- },
- [GRAPHQL]: {
- detailsPath: 'detailsPath',
- groupId: 'name',
- hasDetails: 'hasDetails',
- pipelineStatus: 'status',
- sourceJob: ['sourceJob', 'name'],
- },
-};
-
-const accessValue = (dataMethod, prop, item) => {
- return get(item, accessors[dataMethod][prop]);
-};
-
-export { accessors, accessValue };
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
index dd9cdae518f..0b59612b25c 100644
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ b/app/assets/javascripts/pipelines/components/graph/constants.js
@@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream';
*/
export const ONE_COL_WIDTH = 180;
-export const REST = 'rest';
-export const GRAPHQL = 'graphql';
-
export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
index b2a3f27e079..6f4360649ff 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
@@ -23,6 +23,11 @@ export default {
required: false,
default: -1,
},
+ cssClassJobName: {
+ type: [String, Array],
+ required: false,
+ default: '',
+ },
stageName: {
type: String,
required: false,
@@ -59,7 +64,8 @@ export default {
type="button"
data-toggle="dropdown"
data-display="static"
- class="dropdown-menu-toggle build-content gl-build-content gl-pipeline-job-width! gl-pr-4!"
+ :class="cssClassJobName"
+ class="dropdown-menu-toggle gl-pipeline-job-width! gl-pr-4!"
>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
<job-item
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 6584d89d87c..fd40ca0b9c9 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { accessValue } from './accessors';
-import { REST, SINGLE_JOB } from './constants';
+import { SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@@ -47,18 +46,13 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [delayedJobMixin],
- inject: {
- dataMethod: {
- default: REST,
- },
- },
props: {
job: {
type: Object,
required: true,
},
cssClassJobName: {
- type: String,
+ type: [String, Array],
required: false,
default: '',
},
@@ -111,10 +105,10 @@ export default {
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
},
detailsPath() {
- return accessValue(this.dataMethod, 'detailsPath', this.status);
+ return this.status.detailsPath;
},
hasDetails() {
- return accessValue(this.dataMethod, 'hasDetails', this.status);
+ return this.status.hasDetails;
},
isSingleItem() {
return this.type === SINGLE_JOB;
@@ -189,7 +183,7 @@ export default {
if (this.isSingleItem) {
/*
This is so the jobDropdown still toggles. Issue to refactor:
- https://gitlab.com/gitlab-org/gitlab/-/issues/267117
+ https://gitlab.com/gitlab-org/gitlab/-/issues/267117
*/
evt.stopPropagation();
}
@@ -226,11 +220,11 @@ export default {
<div class="ci-job-name-component gl-display-flex gl-align-items-center">
<ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
<div class="gl-pl-3 gl-display-flex gl-flex-direction-column gl-w-full">
- <div class="gl-text-truncate mw-70p gl-line-height-normal">{{ job.name }}</div>
+ <div class="gl-text-truncate gl-w-70p gl-line-height-normal">{{ job.name }}</div>
<div
v-if="showStageName"
data-testid="stage-name-in-job"
- class="gl-text-truncate mw-70p gl-font-sm gl-text-gray-500 gl-line-height-normal"
+ class="gl-text-truncate gl-w-70p gl-font-sm gl-text-gray-500 gl-line-height-normal"
>
{{ stageName }}
</div>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index dd8a354511a..be47799868b 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
-import { accessValue } from './accessors';
-import { DOWNSTREAM, REST, UPSTREAM } from './constants';
+import { DOWNSTREAM, UPSTREAM } from './constants';
export default {
directives: {
@@ -18,11 +17,6 @@ export default {
GlLoadingIcon,
GlBadge,
},
- inject: {
- dataMethod: {
- default: REST,
- },
- },
props: {
columnTitle: {
type: String,
@@ -40,20 +34,9 @@ export default {
type: String,
required: true,
},
- /*
- The next two props will be removed or required
- once the graph transition is done.
- See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
- */
isLoading: {
type: Boolean,
- required: false,
- default: false,
- },
- projectId: {
- type: Number,
- required: false,
- default: -1,
+ required: true,
},
},
computed: {
@@ -65,7 +48,7 @@ export default {
return `js-linked-pipeline-${this.pipeline.id}`;
},
pipelineStatus() {
- return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline);
+ return this.pipeline.status;
},
projectName() {
return this.pipeline.project.name;
@@ -97,12 +80,10 @@ export default {
return this.type === UPSTREAM;
},
isSameProject() {
- return this.projectId > -1
- ? this.projectId === this.pipeline.project.id
- : !this.pipeline.multiproject;
+ return !this.pipeline.multiproject;
},
sourceJobName() {
- return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
+ return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
@@ -143,9 +124,8 @@ export default {
<div
ref="linkedPipeline"
v-gl-tooltip
- class="linked-pipeline build gl-pipeline-job-width"
+ class="gl-pipeline-job-width"
:title="tooltipText"
- :class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index d251e0d8bd8..3c1208afbf0 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -195,7 +195,7 @@ export default {
<template>
<div class="gl-display-flex">
<div :class="columnClass" class="linked-pipelines-column">
- <div data-testid="linked-column-title" class="stage-name" :class="computedTitleClasses">
+ <div data-testid="linked-column-title" :class="computedTitleClasses">
{{ columnTitle }}
</div>
<ul class="gl-pl-0">
@@ -224,7 +224,7 @@ export default {
<pipeline-graph
v-if="isExpanded(pipeline.id)"
:type="type"
- class="d-inline-block gl-mt-n2"
+ class="gl-inline-block gl-mt-n2"
:config-paths="configPaths"
:pipeline="currentPipeline"
:computed-pipeline-info="getPipelineLayers(pipeline.id)"
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index d34ae8036ed..b0f375c9aeb 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '../../utils';
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
import ActionComponent from '../jobs_shared/action_component.vue';
-import { accessValue } from './accessors';
-import { GRAPHQL } from './constants';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
@@ -65,6 +63,21 @@ export default {
required: true,
},
},
+ jobClasses: [
+ 'gl-py-3',
+ 'gl-px-4',
+ 'gl-border-gray-100',
+ 'gl-border-solid',
+ 'gl-border-1',
+ 'gl-bg-white',
+ 'gl-rounded-7',
+ 'gl-hover-bg-gray-50',
+ 'gl-focus-bg-gray-50',
+ 'gl-hover-text-gray-900',
+ 'gl-focus-text-gray-900',
+ 'gl-hover-border-gray-200',
+ 'gl-focus-border-gray-200',
+ ],
titleClasses: [
'gl-font-weight-bold',
'gl-pipeline-job-width',
@@ -97,7 +110,7 @@ export default {
},
methods: {
getGroupId(group) {
- return accessValue(GRAPHQL, 'groupId', group);
+ return group.name;
},
groupId(group) {
return `ci-badge-${escape(group.name)}`;
@@ -134,7 +147,7 @@ export default {
:action-icon="action.icon"
:tooltip-text="action.title"
:link="action.path"
- class="js-stage-action stage-action rounded"
+ class="js-stage-action"
@pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
/>
</div>
@@ -157,7 +170,7 @@ export default {
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipelineId"
:stage-name="showStageName ? group.stageName : ''"
- css-class-job-name="gl-build-content"
+ :css-class-job-name="$options.jobClasses"
:class="[
{ 'gl-opacity-3': isFadedOut(group.name) },
'gl-transition-duration-slow gl-transition-timing-function-ease',
@@ -169,6 +182,7 @@ export default {
:group="group"
:stage-name="showStageName ? group.stageName : ''"
:pipeline-id="pipelineId"
+ :css-class-job-name="$options.jobClasses"
/>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 5db2b604956..4db6a3c9fd8 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -218,7 +218,7 @@ export default {
:status="pipeline.detailedStatus"
:time="pipeline.createdAt"
:user="pipeline.user"
- :item-id="Number(pipelineId)"
+ :item-id="pipelineId"
item-name="Pipeline"
>
<gl-button
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index 7e7f0572faf..fa7330ce890 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -1,55 +1,18 @@
import { memoize } from 'lodash';
+import { createNodeDict } from '../utils';
import { createSankey } from './dag/drawing_utils';
/*
- The following functions are the main engine in transforming the data as
- received from the endpoint into the format the d3 graph expects.
-
- Input is of the form:
- [nodes]
- nodes: [{category, name, jobs, size}]
- category is the stage name
- name is a group name; in the case that the group has one job, it is
- also the job name
- size is the number of parallel jobs
- jobs: [{ name, needs}]
- job name is either the same as the group name or group x/y
- needs: [job-names]
- needs is an array of job-name strings
-
- Output is of the form:
- { nodes: [node], links: [link] }
- node: { name, category }, + unused info passed through
- link: { source, target, value }, with source & target being node names
- and value being a constant
-
- We create nodes in the GraphQL update function, and then here we create the node dictionary,
- then create links, and then dedupe the links, so that in the case where
- job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
- from job 1 to job 2 then another from job 2 to job 4.
-
- CREATE LINKS
- nodes.name -> target
- nodes.name.needs.each -> source (source is the name of the group, not the parallel job)
- 10 -> value (constant)
- */
-
-export const createNodeDict = (nodes) => {
- return nodes.reduce((acc, node) => {
- const newNode = {
- ...node,
- needs: node.jobs.map((job) => job.needs || []).flat(),
- };
-
- if (node.size > 1) {
- node.jobs.forEach((job) => {
- acc[job.name] = newNode;
- });
- }
+ A peformant alternative to lodash's isEqual. Because findIndex always finds
+ the first instance of a match, if the found index is not the first, we know
+ it is in fact a duplicate.
+*/
+const deduplicate = (item, itemIndex, arr) => {
+ const foundIdx = arr.findIndex((test) => {
+ return test.source === item.source && test.target === item.target;
+ });
- acc[node.name] = newNode;
- return acc;
- }, {});
+ return foundIdx === itemIndex;
};
export const makeLinksFromNodes = (nodes, nodeDict) => {
@@ -83,7 +46,8 @@ export const getAllAncestors = (nodes, nodeDict) => {
return nodeDict[node]?.needs || '';
})
.flat()
- .filter(Boolean);
+ .filter(Boolean)
+ .filter(deduplicate);
if (needs.length) {
return [...needs, ...getAllAncestors(needs, nodeDict)];
@@ -108,29 +72,15 @@ export const filterByAncestors = (links, nodeDict) =>
const targetNode = target;
const targetNodeNeeds = nodeDict[targetNode].needs;
const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source);
-
const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
return !allAncestors.includes(source);
});
-/*
- A peformant alternative to lodash's isEqual. Because findIndex always finds
- the first instance of a match, if the found index is not the first, we know
- it is in fact a duplicate.
-*/
-const deduplicate = (item, itemIndex, arr) => {
- const foundIdx = arr.findIndex((test) => {
- return test.source === item.source && test.target === item.target;
- });
-
- return foundIdx === itemIndex;
-};
-
export const parseData = (nodes) => {
const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict);
- const filteredLinks = filterByAncestors(allLinks, nodeDict);
- const links = filteredLinks.filter(deduplicate);
+ const filteredLinks = allLinks.filter(deduplicate);
+ const links = filterByAncestors(filteredLinks, nodeDict);
return { nodes, links };
};
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 40ee071f1f5..3470c963ade 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
@@ -100,7 +100,7 @@ export default {
<gl-loading-icon v-if="isLoading" size="sm" />
- <gl-dropdown-item v-if="!artifacts.length" data-testid="artifacts-empty-message">
+ <gl-dropdown-item v-if="!artifacts.length && !isLoading" data-testid="artifacts-empty-message">
{{ $options.i18n.emptyArtifactsMessage }}
</gl-dropdown-item>
@@ -110,6 +110,7 @@ export default {
:href="artifact.path"
rel="nofollow"
download
+ class="gl-word-break-word"
data-testid="artifact-item"
>
<gl-sprintf :message="$options.i18n.downloadArtifact">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
index 24b5c85c9d6..3bd149fc782 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlLink, GlModal } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { __, s__, sprintf } from '~/locale';
@@ -72,7 +71,7 @@ export default {
:action-cancel="cancelProps"
@primary="emitSubmit($event)"
>
- <p v-html="modalText"></p>
+ <p v-html="modalText /* eslint-disable-line vue/no-v-html */"></p>
<p v-if="pipeline">
<ci-icon
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index 0b70e74b8ff..2dfdaa0ea28 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -39,7 +39,7 @@ export default {
return this.value.map((i) => i.type);
},
tokens() {
- const tokens = [
+ return [
{
type: this.$options.userType,
icon: 'user',
@@ -77,20 +77,15 @@ export default {
token: PipelineStatusToken,
operators: OPERATOR_IS_ONLY,
},
- ];
-
- if (gon.features.pipelineSourceFilter) {
- tokens.push({
+ {
type: this.$options.sourceType,
icon: 'trigger-source',
title: s__('Pipeline|Source'),
unique: true,
token: PipelineSourceToken,
operators: OPERATOR_IS_ONLY,
- });
- }
-
- return tokens;
+ },
+ ];
},
parsedParams() {
return map(this.params, (val, key) => ({
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index 2475d958e3c..12ee82f0390 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -212,6 +212,7 @@ export default {
<linked-pipelines-mini-list
v-if="item.triggered.length"
:triggered="item.triggered"
+ :pipeline-path="item.path"
data-testid="mini-graph-downstream"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
new file mode 100644
index 00000000000..02baa76f627
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
@@ -0,0 +1,52 @@
+import { s__ } from '~/locale';
+
+export const PIPELINE_SOURCES = [
+ {
+ text: s__('Pipeline|Source|Push'),
+ value: 'push',
+ },
+ {
+ text: s__('Pipeline|Source|Web'),
+ value: 'web',
+ },
+ {
+ text: s__('Pipeline|Source|Trigger'),
+ value: 'trigger',
+ },
+ {
+ text: s__('Pipeline|Source|Schedule'),
+ value: 'schedule',
+ },
+ {
+ text: s__('Pipeline|Source|API'),
+ value: 'api',
+ },
+ {
+ text: s__('Pipeline|Source|External'),
+ value: 'external',
+ },
+ {
+ text: s__('Pipeline|Source|Pipeline'),
+ value: 'pipeline',
+ },
+ {
+ text: s__('Pipeline|Source|Chat'),
+ value: 'chat',
+ },
+ {
+ text: s__('Pipeline|Source|Web IDE'),
+ value: 'webide',
+ },
+ {
+ text: s__('Pipeline|Source|Merge Request'),
+ value: 'merge_request_event',
+ },
+ {
+ text: s__('Pipeline|Source|External Pull Request'),
+ value: 'external_pull_request_event',
+ },
+ {
+ text: s__('Pipeline|Source|Parent Pipeline'),
+ value: 'parent_pipeline',
+ },
+];
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
index 71efa8b2ab4..9643ddfbd21 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
@@ -1,8 +1,9 @@
<script>
import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants';
export default {
+ PIPELINE_SOURCES,
components: {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
@@ -18,68 +19,8 @@ export default {
},
},
computed: {
- sources() {
- return [
- {
- text: s__('Pipeline|Source|Push'),
- value: 'push',
- },
- {
- text: s__('Pipeline|Source|Web'),
- value: 'web',
- },
- {
- text: s__('Pipeline|Source|Trigger'),
- value: 'trigger',
- },
- {
- text: s__('Pipeline|Source|Schedule'),
- value: 'schedule',
- },
- {
- text: s__('Pipeline|Source|API'),
- value: 'api',
- },
- {
- text: s__('Pipeline|Source|External'),
- value: 'external',
- },
- {
- text: s__('Pipeline|Source|Pipeline'),
- value: 'pipeline',
- },
- {
- text: s__('Pipeline|Source|Chat'),
- value: 'chat',
- },
- {
- text: s__('Pipeline|Source|Web IDE'),
- value: 'webide',
- },
- {
- text: s__('Pipeline|Source|Merge Request'),
- value: 'merge_request_event',
- },
- {
- text: s__('Pipeline|Source|External Pull Request'),
- value: 'external_pull_request_event',
- },
- {
- text: s__('Pipeline|Source|Parent Pipeline'),
- value: 'parent_pipeline',
- },
- {
- text: s__('Pipeline|Source|On-Demand DAST Scan'),
- value: 'ondemand_dast_scan',
- },
- {
- text: s__('Pipeline|Source|On-Demand DAST Validation'),
- value: 'ondemand_dast_validation',
- },
- ];
- },
- findActiveSource() {
- return this.sources.find((source) => source.value === this.value.data);
+ activeSource() {
+ return PIPELINE_SOURCES.find((source) => source.value === this.value.data);
},
},
};
@@ -89,13 +30,13 @@ export default {
<gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
<template #view>
<div class="gl-display-flex gl-align-items-center">
- <span>{{ findActiveSource.text }}</span>
+ <span>{{ activeSource.text }}</span>
</div>
</template>
<template #suggestions>
<gl-filtered-search-suggestion
- v-for="source in sources"
+ v-for="source in $options.PIPELINE_SOURCES"
:key="source.value"
:value="source.value"
>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index c6e767d5424..ee9560e36c4 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,16 +1,10 @@
-import Vue from 'vue';
import createFlash from '~/flash';
-import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import Translate from '~/vue_shared/translate';
-import TestReports from './components/test_reports/test_reports.vue';
import createDagApp from './pipeline_details_dag';
import { createPipelinesDetailApp } from './pipeline_details_graph';
import { createPipelineHeaderApp } from './pipeline_details_header';
import { apolloProvider } from './pipeline_shared_client';
-import createTestReportsStore from './stores/test_reports';
-
-Vue.use(Translate);
+import { createTestDetails } from './pipeline_test_details';
const SELECTORS = {
PIPELINE_DETAILS: '.js-pipeline-details-vue',
@@ -19,33 +13,6 @@ const SELECTORS = {
PIPELINE_TESTS: '#js-pipeline-tests-detail',
};
-const createTestDetails = () => {
- const el = document.querySelector(SELECTORS.PIPELINE_TESTS);
- const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
- el?.dataset || {};
- const testReportsStore = createTestReportsStore({
- blobPath,
- summaryEndpoint,
- suiteEndpoint,
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- TestReports,
- },
- provide: {
- emptyStateImagePath,
- hasTestReport: parseBoolean(hasTestReport),
- },
- store: testReportsStore,
- render(createElement) {
- return createElement('test-reports');
- },
- });
-};
-
export default async function initPipelineDetailsBundle() {
const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
@@ -65,6 +32,27 @@ export default async function initPipelineDetailsBundle() {
});
}
- createDagApp(apolloProvider);
- createTestDetails();
+ try {
+ createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
+ } catch {
+ createFlash({
+ message: __('An error occurred while loading a section of this page.'),
+ });
+ }
+
+ try {
+ createDagApp(apolloProvider);
+ } catch {
+ createFlash({
+ message: __('An error occurred while loading the Needs tab.'),
+ });
+ }
+
+ try {
+ createTestDetails(SELECTORS.PIPELINE_TESTS);
+ } catch {
+ createFlash({
+ message: __('An error occurred while loading the Test Reports tab.'),
+ });
+ }
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js
index 39c3c2ea5c5..9dd5cd7b281 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_graph.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_graph.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GRAPHQL } from './components/graph/constants';
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { reportToSentry } from './utils';
@@ -23,7 +22,6 @@ const createPipelinesDetailApp = (
pipelineProjectPath,
pipelineIid,
graphqlResourceEtag,
- dataMethod: GRAPHQL,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
diff --git a/app/assets/javascripts/pipelines/pipeline_test_details.js b/app/assets/javascripts/pipelines/pipeline_test_details.js
new file mode 100644
index 00000000000..46c7ec07d03
--- /dev/null
+++ b/app/assets/javascripts/pipelines/pipeline_test_details.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '~/vue_shared/translate';
+import TestReports from './components/test_reports/test_reports.vue';
+import createTestReportsStore from './stores/test_reports';
+
+Vue.use(Translate);
+
+export const createTestDetails = (selector) => {
+ const el = document.querySelector(selector);
+ const { blobPath, emptyStateImagePath, hasTestReport, summaryEndpoint, suiteEndpoint } =
+ el?.dataset || {};
+ const testReportsStore = createTestReportsStore({
+ blobPath,
+ summaryEndpoint,
+ suiteEndpoint,
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ TestReports,
+ },
+ provide: {
+ emptyStateImagePath,
+ hasTestReport: parseBoolean(hasTestReport),
+ },
+ store: testReportsStore,
+ render(createElement) {
+ return createElement('test-reports');
+ },
+ });
+};
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index 02a9e5b7fc6..e28eb74fb1b 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,8 +1,58 @@
import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
-import { createNodeDict } from './components/parsing_utils';
import { SUPPORTED_FILTER_PARAMETERS } from './constants';
+/*
+ The following functions are the main engine in transforming the data as
+ received from the endpoint into the format the d3 graph expects.
+
+ Input is of the form:
+ [nodes]
+ nodes: [{category, name, jobs, size}]
+ category is the stage name
+ name is a group name; in the case that the group has one job, it is
+ also the job name
+ size is the number of parallel jobs
+ jobs: [{ name, needs}]
+ job name is either the same as the group name or group x/y
+ needs: [job-names]
+ needs is an array of job-name strings
+
+ Output is of the form:
+ { nodes: [node], links: [link] }
+ node: { name, category }, + unused info passed through
+ link: { source, target, value }, with source & target being node names
+ and value being a constant
+
+ We create nodes in the GraphQL update function, and then here we create the node dictionary,
+ then create links, and then dedupe the links, so that in the case where
+ job 4 depends on job 1 and job 2, and job 2 depends on job 1, we show only a single link
+ from job 1 to job 2 then another from job 2 to job 4.
+
+ CREATE LINKS
+ nodes.name -> target
+ nodes.name.needs.each -> source (source is the name of the group, not the parallel job)
+ 10 -> value (constant)
+ */
+
+export const createNodeDict = (nodes) => {
+ return nodes.reduce((acc, node) => {
+ const newNode = {
+ ...node,
+ needs: node.jobs.map((job) => job.needs || []).flat(),
+ };
+
+ if (node.size > 1) {
+ node.jobs.forEach((job) => {
+ acc[job.name] = newNode;
+ });
+ }
+
+ acc[node.name] = newNode;
+ return acc;
+ }, {});
+};
+
export const validateParams = (params) => {
return pickBy(params, (val, key) => SUPPORTED_FILTER_PARAMETERS.includes(key) && val);
};
diff --git a/app/assets/javascripts/popovers/components/popovers.vue b/app/assets/javascripts/popovers/components/popovers.vue
index 05a209a97ad..a758503b56b 100644
--- a/app/assets/javascripts/popovers/components/popovers.vue
+++ b/app/assets/javascripts/popovers/components/popovers.vue
@@ -1,11 +1,5 @@
<script>
-// We can't use v-safe-html here as the popover's title or content might contains SVGs that would
-// be stripped by the directive's sanitizer. Instead, we fallback on v-html and we use GitLab's
-// dompurify config that lets SVGs be rendered properly.
-// Context: https://gitlab.com/gitlab-org/gitlab/-/issues/247207
-/* eslint-disable vue/no-v-html */
-import { GlPopover } from '@gitlab/ui';
-import { sanitize } from '~/lib/dompurify';
+import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
const newPopover = (element) => {
const { content, html, placement, title, triggers = 'focus' } = element.dataset;
@@ -24,6 +18,9 @@ export default {
components: {
GlPopover,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
data() {
return {
popovers: [],
@@ -71,9 +68,9 @@ export default {
popoverExists(element) {
return this.popovers.some((popover) => popover.target === element);
},
- getSafeHtml(html) {
- return sanitize(html);
- },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
},
};
</script>
@@ -82,10 +79,10 @@ export default {
<div>
<gl-popover v-for="(popover, index) in popovers" :key="index" v-bind="popover">
<template #title>
- <span v-if="popover.html" v-html="getSafeHtml(popover.title)"></span>
+ <span v-if="popover.html" v-safe-html:[$options.safeHtmlConfig]="popover.title"></span>
<span v-else>{{ popover.title }}</span>
</template>
- <span v-if="popover.html" v-html="getSafeHtml(popover.content)"></span>
+ <span v-if="popover.html" v-safe-html:[$options.safeHtmlConfig]="popover.content"></span>
<span v-else>{{ popover.content }}</span>
</gl-popover>
</div>
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index 4b14df21f05..fd45d643ecc 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -30,7 +30,7 @@ export default class ProjectSelectComboButton {
}
initLocalStorage() {
- const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
+ const localStorageIsSafe = AccessorUtilities.canUseLocalStorage();
if (localStorageIsSafe) {
this.localStorageKey = [
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 a4a1cb5584d..da14b1e8470 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
@@ -87,6 +87,7 @@ export default {
<linked-pipelines-mini-list
v-if="hasDownstream"
:triggered="downstreamPipelines"
+ :pipeline-path="pipeline.path"
data-testid="commit-box-mini-graph-downstream"
/>
</div>
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
index f7e930bb3f2..ee18c70b6fd 100644
--- a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
+++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql
@@ -1,6 +1,7 @@
query getLinkedPipelines($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
pipeline(iid: $iid) {
+ path
downstream {
nodes {
id
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
index 1d4ec4c110b..2505c47147f 100644
--- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js
@@ -5,7 +5,12 @@ import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(
+ {},
+ {
+ assumeImmutableResults: true,
+ },
+ ),
});
export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => {
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index d3cadcd2bd5..ecd2288eb2f 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -199,6 +199,16 @@ export default {
},
];
},
+ chartOptions() {
+ return {
+ ...this.$options.timesChartOptions,
+ yAxis: {
+ axisLabel: {
+ formatter: (value) => value,
+ },
+ },
+ };
+ },
},
methods: {
hideAlert() {
@@ -314,7 +324,7 @@ export default {
<strong>{{ __('Pipeline durations for the last 30 commits') }}</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
- :option="$options.timesChartOptions"
+ :option="chartOptions"
:bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js
index 5f5ee44c204..f7ea89068a0 100644
--- a/app/assets/javascripts/projects/pipelines/charts/index.js
+++ b/app/assets/javascripts/projects/pipelines/charts/index.js
@@ -7,7 +7,7 @@ import ProjectPipelinesCharts from './components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
const mountPipelineChartsApp = (el) => {
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index ee02f446795..ebd20583a1c 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -71,6 +71,17 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
}
};
+const bindHowToImport = () => {
+ $('.how_to_import_link').on('click', (e) => {
+ e.preventDefault();
+ $(e.currentTarget).next('.modal').show();
+ });
+
+ $('.modal-header .close').on('click', () => {
+ $('.modal').hide();
+ });
+};
+
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
@@ -88,14 +99,7 @@ const bindEvents = () => {
return;
}
- $('.how_to_import_link').on('click', (e) => {
- e.preventDefault();
- $(e.currentTarget).next('.modal').show();
- });
-
- $('.modal-header .close').on('click', () => {
- $('.modal').hide();
- });
+ bindHowToImport();
$('.btn_import_gitlab_project').on('click', () => {
const importHref = $('a.btn_import_gitlab_project').attr('href');
@@ -174,3 +178,5 @@ export default {
onProjectNameChange,
onProjectPathChange,
};
+
+export { bindHowToImport };
diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
index e4edb950a1e..91d8fca0487 100644
--- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
+++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
@@ -43,6 +43,7 @@ export default {
isSharedRunnerEnabled: this.isEnabled,
errorMessage: null,
successfulValidation: false,
+ ccAlertDismissed: false,
};
},
computed: {
@@ -50,7 +51,8 @@ export default {
return (
this.isCreditCardValidationRequired &&
!this.isSharedRunnerEnabled &&
- !this.successfulValidation
+ !this.successfulValidation &&
+ !this.ccAlertDismissed
);
},
},
@@ -89,6 +91,7 @@ export default {
class="gl-pb-5"
:custom-message="$options.i18n.REQUIRES_VALIDATION_TEXT"
@verifiedCreditCard="creditCardValidated"
+ @dismiss="ccAlertDismissed = true"
/>
<gl-toggle
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 34d53e2de0c..fe2d376f1da 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -1,5 +1,13 @@
<script>
-import { GlButton, GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import {
+ GlButton,
+ GlFormSelect,
+ GlToggle,
+ GlLoadingIcon,
+ GlSprintf,
+ GlFormInput,
+ GlLink,
+} from '@gitlab/ui';
import { __ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -14,6 +22,8 @@ export default {
GlToggle,
GlLoadingIcon,
GlSprintf,
+ GlFormInput,
+ GlLink,
},
props: {
isEnabled: {
@@ -148,17 +158,37 @@ export default {
<span class="sr-only">{{ __('Fetching incoming email') }}</span>
</template>
- <template v-if="hasProjectKeySupport">
- <label for="service-desk-project-suffix" class="mt-3">
- {{ __('Project name suffix') }}
- </label>
- <input id="service-desk-project-suffix" v-model.trim="projectKey" class="form-control" />
- <span class="form-text text-muted">
- {{
- __('A string appended to the project path to form the Service Desk email address.')
- }}
- </span>
- </template>
+ <label for="service-desk-project-suffix" class="mt-3">
+ {{ __('Project name suffix') }}
+ </label>
+ <gl-form-input
+ v-if="hasProjectKeySupport"
+ id="service-desk-project-suffix"
+ v-model.trim="projectKey"
+ data-testid="project-suffix"
+ class="form-control"
+ />
+ <span v-if="hasProjectKeySupport" class="form-text text-muted">
+ {{ __('A string appended to the project path to form the Service Desk email address.') }}
+ </span>
+ <span v-else class="form-text text-muted">
+ <gl-sprintf
+ :message="
+ __(
+ 'To add a custom suffix, set up a Service Desk email address. %{linkStart}Learn more.%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link
+ href="https://docs.gitlab.com/ee/user/project/service_desk.html#using-a-custom-email-address"
+ target="_blank"
+ class="gl-text-blue-600 font-size-inherit"
+ >{{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
<label for="service-desk-template-select" class="mt-3">
{{ __('Template to append to all Service Desk issues') }}
diff --git a/app/assets/javascripts/projects/storage_counter/components/app.vue b/app/assets/javascripts/projects/storage_counter/components/app.vue
new file mode 100644
index 00000000000..1a911ea3d9b
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/components/app.vue
@@ -0,0 +1,106 @@
+<script>
+import { GlAlert, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import UsageGraph from '~/vue_shared/components/storage_counter/usage_graph.vue';
+import {
+ ERROR_MESSAGE,
+ LEARN_MORE_LABEL,
+ USAGE_QUOTAS_LABEL,
+ TOTAL_USAGE_TITLE,
+ TOTAL_USAGE_SUBTITLE,
+ TOTAL_USAGE_DEFAULT_TEXT,
+ HELP_LINK_ARIA_LABEL,
+} from '../constants';
+import getProjectStorageCount from '../queries/project_storage.query.graphql';
+import { parseGetProjectStorageResults } from '../utils';
+import StorageTable from './storage_table.vue';
+
+export default {
+ name: 'StorageCounterApp',
+ components: {
+ GlAlert,
+ GlLink,
+ GlLoadingIcon,
+ StorageTable,
+ UsageGraph,
+ },
+ inject: ['projectPath', 'helpLinks'],
+ apollo: {
+ project: {
+ query: getProjectStorageCount,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return parseGetProjectStorageResults(data, this.helpLinks);
+ },
+ error() {
+ this.error = ERROR_MESSAGE;
+ },
+ },
+ },
+ data() {
+ return {
+ project: {},
+ error: '',
+ };
+ },
+ computed: {
+ totalUsage() {
+ return this.project?.storage?.totalUsage || TOTAL_USAGE_DEFAULT_TEXT;
+ },
+ storageTypes() {
+ return this.project?.storage?.storageTypes || [];
+ },
+ },
+ methods: {
+ clearError() {
+ this.error = '';
+ },
+ helpLinkAriaLabel(linkTitle) {
+ return sprintf(HELP_LINK_ARIA_LABEL, {
+ linkTitle,
+ });
+ },
+ },
+ LEARN_MORE_LABEL,
+ USAGE_QUOTAS_LABEL,
+ TOTAL_USAGE_TITLE,
+ TOTAL_USAGE_SUBTITLE,
+};
+</script>
+<template>
+ <gl-loading-icon v-if="$apollo.queries.project.loading" class="gl-mt-5" size="md" />
+ <gl-alert v-else-if="error" variant="danger" @dismiss="clearError">
+ {{ error }}
+ </gl-alert>
+ <div v-else>
+ <div class="gl-pt-5 gl-px-3">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <div>
+ <p class="gl-m-0 gl-font-lg gl-font-weight-bold">{{ $options.TOTAL_USAGE_TITLE }}</p>
+ <p class="gl-m-0 gl-text-gray-400">
+ {{ $options.TOTAL_USAGE_SUBTITLE }}
+ <gl-link
+ :href="helpLinks.usageQuotasHelpPagePath"
+ target="_blank"
+ :aria-label="helpLinkAriaLabel($options.USAGE_QUOTAS_LABEL)"
+ data-testid="usage-quotas-help-link"
+ >
+ {{ $options.LEARN_MORE_LABEL }}
+ </gl-link>
+ </p>
+ </div>
+ <p class="gl-m-0 gl-font-size-h-display gl-font-weight-bold" data-testid="total-usage">
+ {{ totalUsage }}
+ </p>
+ </div>
+ </div>
+ <div v-if="project.statistics" class="gl-w-full">
+ <usage-graph :root-storage-statistics="project.statistics" :limit="0" />
+ </div>
+ <storage-table :storage-types="storageTypes" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
new file mode 100644
index 00000000000..7047fd925fb
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlLink, GlIcon, GlTable, GlSprintf } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { thWidthClass } from '~/lib/utils/table_utility';
+import { sprintf } from '~/locale';
+import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants';
+
+export default {
+ name: 'StorageTable',
+ components: {
+ GlLink,
+ GlIcon,
+ GlTable,
+ GlSprintf,
+ },
+ props: {
+ storageTypes: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ helpLinkAriaLabel(linkTitle) {
+ return sprintf(HELP_LINK_ARIA_LABEL, {
+ linkTitle,
+ });
+ },
+ },
+ projectTableFields: [
+ {
+ key: 'storageType',
+ label: PROJECT_TABLE_LABELS.STORAGE_TYPE,
+ thClass: thWidthClass(90),
+ sortable: true,
+ },
+ {
+ key: 'value',
+ label: PROJECT_TABLE_LABELS.VALUE,
+ thClass: thWidthClass(10),
+ sortable: true,
+ formatter: (value) => {
+ return numberToHumanSize(value, 1);
+ },
+ },
+ ],
+};
+</script>
+<template>
+ <gl-table :items="storageTypes" :fields="$options.projectTableFields">
+ <template #cell(storageType)="{ item }">
+ <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`">
+ {{ item.storageType.name }}
+ <gl-link
+ v-if="item.storageType.helpPath"
+ :href="item.storageType.helpPath"
+ target="_blank"
+ :aria-label="helpLinkAriaLabel(item.storageType.name)"
+ :data-testid="`${item.storageType.id}-help-link`"
+ >
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ </p>
+ <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`">
+ {{ item.storageType.description }}
+ </p>
+ <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm">
+ <gl-icon name="warning" :size="12" />
+ <gl-sprintf :message="item.storageType.warningMessage">
+ <template #warningLink="{ content }">
+ <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js
new file mode 100644
index 00000000000..d9b28abfbe7
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/constants.js
@@ -0,0 +1,61 @@
+import { s__, __ } from '~/locale';
+
+export const PROJECT_STORAGE_TYPES = [
+ {
+ id: 'buildArtifactsSize',
+ name: s__('UsageQuota|Artifacts'),
+ description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'),
+ warningMessage: s__(
+ 'UsageQuota|There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.',
+ ),
+ warningLink: 'https://gitlab.com/groups/gitlab-org/-/epics/5380',
+ },
+ {
+ id: 'lfsObjectsSize',
+ name: s__('UsageQuota|LFS Storage'),
+ description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'),
+ },
+ {
+ id: 'packagesSize',
+ name: s__('UsageQuota|Packages'),
+ description: s__('UsageQuota|Code packages and container images.'),
+ },
+ {
+ id: 'repositorySize',
+ name: s__('UsageQuota|Repository'),
+ description: s__('UsageQuota|Git repository, managed by the Gitaly service.'),
+ },
+ {
+ id: 'snippetsSize',
+ name: s__('UsageQuota|Snippets'),
+ description: s__('UsageQuota|Shared bits of code and text.'),
+ },
+ {
+ id: 'uploadsSize',
+ name: s__('UsageQuota|Uploads'),
+ description: s__('UsageQuota|File attachments and smaller design graphics.'),
+ },
+ {
+ id: 'wikiSize',
+ name: s__('UsageQuota|Wiki'),
+ description: s__('UsageQuota|Wiki content.'),
+ },
+];
+
+export const PROJECT_TABLE_LABELS = {
+ STORAGE_TYPE: s__('UsageQuota|Storage type'),
+ VALUE: s__('UsageQuota|Usage'),
+};
+
+export const ERROR_MESSAGE = s__(
+ 'UsageQuota|Something went wrong while fetching project storage statistics',
+);
+
+export const LEARN_MORE_LABEL = s__('Learn more.');
+export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas');
+export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link');
+export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A');
+export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage Breakdown');
+export const TOTAL_USAGE_SUBTITLE = s__(
+ 'UsageQuota|Includes project registry, artifacts, packages, wiki, uploads and other items.',
+);
diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js
new file mode 100644
index 00000000000..10668f08402
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/index.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import StorageCounterApp from './components/app.vue';
+
+Vue.use(VueApollo);
+
+export default (containerId = 'js-project-storage-count-app') => {
+ const el = document.getElementById(containerId);
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ projectPath,
+ usageQuotasHelpPagePath,
+ buildArtifactsHelpPagePath,
+ lfsObjectsHelpPagePath,
+ packagesHelpPagePath,
+ repositoryHelpPagePath,
+ snippetsHelpPagePath,
+ uploadsHelpPagePath,
+ wikiHelpPagePath,
+ } = el.dataset;
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
+ });
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ helpLinks: {
+ usageQuotasHelpPagePath,
+ buildArtifactsHelpPagePath,
+ lfsObjectsHelpPagePath,
+ packagesHelpPagePath,
+ repositoryHelpPagePath,
+ snippetsHelpPagePath,
+ uploadsHelpPagePath,
+ wikiHelpPagePath,
+ },
+ },
+ render(createElement) {
+ return createElement(StorageCounterApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql b/app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql
new file mode 100644
index 00000000000..a4f2c529522
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/queries/project_storage.query.graphql
@@ -0,0 +1,16 @@
+query getProjectStorageCount($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ statistics {
+ buildArtifactsSize
+ pipelineArtifactsSize
+ lfsObjectsSize
+ packagesSize
+ repositorySize
+ snippetsSize
+ storageSize
+ uploadsSize
+ wikiSize
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/storage_counter/utils.js b/app/assets/javascripts/projects/storage_counter/utils.js
new file mode 100644
index 00000000000..cb26603fff5
--- /dev/null
+++ b/app/assets/javascripts/projects/storage_counter/utils.js
@@ -0,0 +1,40 @@
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { PROJECT_STORAGE_TYPES } from './constants';
+
+/**
+ * This method parses the results from `getProjectStorageCount` call.
+ *
+ * @param {Object} data graphql result
+ * @returns {Object}
+ */
+export const parseGetProjectStorageResults = (data, helpLinks) => {
+ const projectStatistics = data?.project?.statistics;
+ if (!projectStatistics) {
+ return {};
+ }
+ const { storageSize, ...storageStatistics } = projectStatistics;
+ const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => {
+ if (!storageStatistics[currentType.id]) {
+ return types;
+ }
+
+ const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`);
+ const helpPath = helpLinks[helpPathKey];
+
+ return types.concat({
+ storageType: {
+ ...currentType,
+ helpPath,
+ },
+ value: storageStatistics[currentType.id],
+ });
+ }, []);
+
+ return {
+ storage: {
+ totalUsage: numberToHumanSize(storageSize, 1),
+ storageTypes,
+ },
+ statistics: projectStatistics,
+ };
+};
diff --git a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
index 02e31d6fbb3..668cc10c454 100644
--- a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
+++ b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
@@ -1,8 +1,12 @@
<script>
import { GlBanner } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { setCookie } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { EVENT_LABEL, DISMISS_EVENT, CLICK_EVENT } from '../constants';
+
+const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
export default {
name: 'TerraformNotification',
@@ -15,37 +19,42 @@ export default {
},
components: {
GlBanner,
+ UserCalloutDismisser,
},
- inject: ['terraformImagePath', 'bannerDismissedKey'],
- data() {
- return {
- isVisible: true,
- };
- },
+ mixins: [trackingMixin],
+ inject: ['terraformImagePath'],
computed: {
docsUrl() {
- return helpPagePath('user/infrastructure/terraform_state');
+ return helpPagePath('user/infrastructure/iac/terraform_state.md');
},
},
methods: {
handleClose() {
- setCookie(this.bannerDismissedKey, true);
- this.isVisible = false;
+ this.track(DISMISS_EVENT);
+ this.$refs.calloutDismisser.dismiss();
+ },
+ buttonClick() {
+ this.track(CLICK_EVENT);
},
},
};
</script>
<template>
- <div v-if="isVisible" class="gl-py-5">
- <gl-banner
- :title="$options.i18n.title"
- :button-text="$options.i18n.buttonText"
- :button-link="docsUrl"
- :svg-path="terraformImagePath"
- variant="promotion"
- @close="handleClose"
- >
- <p>{{ $options.i18n.description }}</p>
- </gl-banner>
- </div>
+ <user-callout-dismisser ref="calloutDismisser" feature-name="terraform_notification_dismissed">
+ <template #default="{ shouldShowCallout }">
+ <div v-if="shouldShowCallout" class="gl-py-5">
+ <gl-banner
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.buttonText"
+ :button-link="docsUrl"
+ :svg-path="terraformImagePath"
+ variant="promotion"
+ @primary="buttonClick"
+ @close="handleClose"
+ >
+ <p>{{ $options.i18n.description }}</p>
+ </gl-banner>
+ </div>
+ </template>
+ </user-callout-dismisser>
</template>
diff --git a/app/assets/javascripts/projects/terraform_notification/constants.js b/app/assets/javascripts/projects/terraform_notification/constants.js
new file mode 100644
index 00000000000..029f40b2ab2
--- /dev/null
+++ b/app/assets/javascripts/projects/terraform_notification/constants.js
@@ -0,0 +1,3 @@
+export const EVENT_LABEL = 'terraform_banner';
+export const DISMISS_EVENT = 'dismiss_banner';
+export const CLICK_EVENT = 'click_button';
diff --git a/app/assets/javascripts/projects/terraform_notification/index.js b/app/assets/javascripts/projects/terraform_notification/index.js
index 0a273247930..362e71ed902 100644
--- a/app/assets/javascripts/projects/terraform_notification/index.js
+++ b/app/assets/javascripts/projects/terraform_notification/index.js
@@ -1,12 +1,18 @@
import Vue from 'vue';
-import { parseBoolean, getCookie } from '~/lib/utils/common_utils';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import TerraformNotification from './components/terraform_notification.vue';
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
export default () => {
const el = document.querySelector('.js-terraform-notification');
- const bannerDismissedKey = 'terraform_notification_dismissed';
- if (!el || parseBoolean(getCookie(bannerDismissedKey))) {
+ if (!el) {
return false;
}
@@ -14,9 +20,9 @@ export default () => {
return new Vue({
el,
+ apolloProvider,
provide: {
terraformImagePath,
- bannerDismissedKey,
},
render: (createElement) => createElement(TerraformNotification),
});
diff --git a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
index c1dae75801e..eecb3573046 100644
--- a/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
+++ b/app/assets/javascripts/prometheus_alerts/components/reset_key.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButton, GlFormGroup, GlFormInput, GlModal, GlModalDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -85,7 +84,7 @@ export default {
</p>
</div>
<div class="col-lg-9">
- <p v-html="sectionDescription"></p>
+ <p v-html="sectionDescription /* eslint-disable-line vue/no-v-html */"></p>
<gl-form-group :label="__('URL')" label-for="notify-url" label-class="label-bold">
<div class="input-group">
<gl-form-input id="notify-url" :readonly="true" :value="notifyUrl" />
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index d0d2c1400a7..d4b52860261 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -12,7 +12,7 @@ export default class ProtectedBranchCreate {
this.hasLicense = options.hasLicense;
this.$form = $('.js-new-protected-branch');
- this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
+ this.isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
this.currentProjectUserDefaults = {};
this.buildDropdowns();
this.$forcePushToggle = this.$form.find('.js-force-push-toggle');
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index 82963fe98fd..ce781c64006 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -149,8 +149,7 @@ export default {
// This method is defined here instead of in `methods`
// because we need to access the .cancel() method
// lodash attaches to the function, which is
- // made inaccessible by Vue. More info:
- // https://stackoverflow.com/a/52988020/1063392
+ // made inaccessible by Vue.
this.debouncedSearch = debounce(function search() {
this.search();
}, SEARCH_DEBOUNCE_MS);
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index 68bca2fc6b9..3201ca1f443 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import $ from 'jquery';
import { isEmpty } from 'lodash';
import { scrollToElement } from '~/lib/utils/common_utils';
@@ -103,7 +102,10 @@ export default {
<evidence-block v-if="hasEvidence" :release="release" />
<div ref="gfm-content" class="card-text gl-mt-3">
- <div class="md" v-html="release.descriptionHtml"></div>
+ <div
+ class="md"
+ v-html="release.descriptionHtml /* eslint-disable-line vue/no-v-html */"
+ ></div>
</div>
</div>
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 6014d9d6ad8..04e72809e62 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,18 +1,16 @@
import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
-import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
-import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
-import TestIssueBody from '../grouped_test_report/components/test_issue_body.vue';
export const components = {
- AccessibilityIssueBody,
- CodequalityIssueBody,
- TestIssueBody,
+ 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.name,
- CodequalityIssueBody: CodequalityIssueBody.name,
- TestIssueBody: TestIssueBody.name,
+ AccessibilityIssueBody: 'AccessibilityIssueBody',
+ CodequalityIssueBody: 'CodequalityIssueBody',
+ TestIssueBody: 'TestIssueBody',
};
export const iconComponents = {
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue
index 8871da8fbd7..918263bfb5c 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -53,11 +53,7 @@ export default {
};
</script>
<template>
- <li
- :class="{ 'is-dismissed': issue.isDismissed }"
- class="report-block-list-issue align-items-center"
- data-qa-selector="report_item_row"
- >
+ <li class="report-block-list-issue align-items-center" data-qa-selector="report_item_row">
<component
:is="iconComponent"
v-if="showReportSectionStatusIcon"
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 665b0698cc0..1d79818cbe8 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -118,7 +118,7 @@ export default {
return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
},
isBinaryFileType() {
- return this.isBinary || this.viewer.fileType === 'download';
+ return this.isBinary || this.blobInfo.simpleViewer?.fileType !== 'text';
},
blobInfo() {
const nodes = this.project?.repository?.blobs?.nodes || [];
@@ -180,7 +180,7 @@ export default {
<div v-if="blobInfo && !isLoading" class="file-holder">
<blob-header
:blob="blobInfo"
- :hide-viewer-switcher="!hasRichViewer || isBinary"
+ :hide-viewer-switcher="!hasRichViewer || isBinaryFileType"
:is-binary="isBinaryFileType"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@@ -188,7 +188,7 @@ export default {
>
<template #actions>
<blob-edit
- :show-edit-button="!isBinary"
+ :show-edit-button="!isBinaryFileType"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
/>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
new file mode 100644
index 00000000000..83d36209bb3
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
@@ -0,0 +1,19 @@
+<script>
+export default {
+ props: {
+ url: {
+ type: String,
+ required: true,
+ },
+ alt: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-text-center gl-p-7 gl-bg-gray-50">
+ <img :src="url" :alt="alt" data-testid="image" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index 4e16b16041f..3b4f4eb51fe 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -6,6 +6,8 @@ export const loadViewer = (type) => {
return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
case 'download':
return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
+ case 'image':
+ return () => import(/* webpackChunkName: 'blob_image_viewer' */ './image_viewer.vue');
default:
return null;
}
@@ -23,5 +25,9 @@ export const viewerProps = (type, blob) => {
filePath: blob.rawPath,
fileSize: blob.rawSize,
},
+ image: {
+ url: blob.rawPath,
+ alt: blob.name,
+ },
}[type];
};
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index a7176853819..5c713796bd6 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
@@ -125,7 +124,7 @@ export default {
:href="commit.webPath"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
- v-html="commit.titleHtml"
+ v-html="commit.titleHtml /* eslint-disable-line vue/no-v-html */"
/>
<gl-button
v-if="commit.descriptionHtml"
@@ -153,11 +152,14 @@ export default {
v-if="commitDescription"
:class="{ 'd-block': showDescription }"
class="commit-row-description gl-mb-3"
- v-html="commitDescription"
+ v-html="commitDescription /* eslint-disable-line vue/no-v-html */"
></pre>
</div>
<div class="commit-actions flex-row">
- <div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div>
+ <div
+ v-if="commit.signatureHtml"
+ v-html="commit.signatureHtml /* eslint-disable-line vue/no-v-html */"
+ ></div>
<div v-if="commit.pipeline" class="ci-status-link">
<gl-link
v-gl-tooltip.left
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index b74c2333148..54e67c5ab5c 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
@@ -60,7 +59,11 @@ export default {
</div>
<div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about">
<gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" />
- <div v-else-if="readme" ref="readme" v-html="readme.html"></div>
+ <div
+ v-else-if="readme"
+ ref="readme"
+ v-html="readme.html /* eslint-disable-line vue/no-v-html */"
+ ></div>
</div>
</article>
</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 69eefc807d7..10a30bd44b1 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -100,9 +100,9 @@ export default {
/>
<template v-for="val in entries">
<table-row
- v-for="entry in val"
+ v-for="(entry, index) in val"
:id="entry.id"
- :key="`${entry.flatPath}-${entry.id}`"
+ :key="`${entry.flatPath}-${entry.id}-${index}`"
:sha="entry.sha"
:project-path="projectPath"
:current-path="path"
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index fa358a75cc1..009dd19b4a5 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import {
GlBadge,
GlLink,
@@ -11,6 +10,7 @@ import {
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
import filesQuery from 'shared_queries/repository/files.query.graphql';
+import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { TREE_PAGE_SIZE } from '~/repository/constants';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -154,7 +154,8 @@ export default {
return this.isFolder ? this.loadFolder() : this.loadBlob();
},
loadFolder() {
- this.apolloQuery(filesQuery, {
+ const query = this.glFeatures.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery;
+ this.apolloQuery(query, {
projectPath: this.projectPath,
ref: this.ref,
path: this.path,
@@ -230,7 +231,7 @@ export default {
:href="commit.commitPath"
:title="commit.message"
class="str-truncated-100 tree-commit-link"
- v-html="commit.titleHtml"
+ v-html="commit.titleHtml /* eslint-disable-line vue/no-v-html */"
/>
<gl-skeleton-loading v-else :lines="1" class="h-auto" />
</td>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index c861fb8dd06..5a8ead9ae8f 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,5 +1,6 @@
<script>
import filesQuery from 'shared_queries/repository/files.query.graphql';
+import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import createFlash from '~/flash';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '../../locale';
@@ -69,6 +70,9 @@ export default {
hasShowMore() {
return !this.clickedShowMore && this.pageLimitReached;
},
+ paginatedTreeEnabled() {
+ return this.glFeatures.paginatedTreeGraphqlQuery;
+ },
},
watch: {
@@ -91,7 +95,7 @@ export default {
return this.$apollo
.query({
- query: filesQuery,
+ query: this.paginatedTreeEnabled ? paginatedTreeQuery : filesQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
@@ -104,13 +108,20 @@ export default {
if (data.errors) throw data.errors;
if (!data?.project?.repository || originalPath !== (this.path || '/')) return;
- const pageInfo = this.hasNextPage(data.project.repository.tree);
+ const pageInfo = this.paginatedTreeEnabled
+ ? data.project.repository.paginatedTree.pageInfo
+ : this.hasNextPage(data.project.repository.tree);
this.isLoadingFiles = false;
this.entries = Object.keys(this.entries).reduce(
(acc, key) => ({
...acc,
- [key]: this.normalizeData(key, data.project.repository.tree[key].edges),
+ [key]: this.normalizeData(
+ key,
+ this.paginatedTreeEnabled
+ ? data.project.repository.paginatedTree.nodes[0][key]
+ : data.project.repository.tree[key].edges,
+ ),
}),
{},
);
@@ -132,7 +143,9 @@ export default {
});
},
normalizeData(key, data) {
- return this.entries[key].concat(data.map(({ node }) => node));
+ return this.entries[key].concat(
+ this.paginatedTreeEnabled ? data.nodes : data.map(({ node }) => node),
+ );
},
hasNextPage(data) {
return []
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index b536bcb1875..93032bf17e2 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -11,3 +11,5 @@ export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these c
export const COMMIT_MESSAGE_SUBJECT_MAX_LENGTH = 52;
export const COMMIT_MESSAGE_BODY_MAX_LENGTH = 72;
+
+export const LIMITED_CONTAINER_WIDTH_CLASS = 'limit-container-width';
diff --git a/app/assets/javascripts/repository/mixins/preload.js b/app/assets/javascripts/repository/mixins/preload.js
index ffc260ec84f..a2ddcbf0e4c 100644
--- a/app/assets/javascripts/repository/mixins/preload.js
+++ b/app/assets/javascripts/repository/mixins/preload.js
@@ -1,4 +1,5 @@
import filesQuery from 'shared_queries/repository/files.query.graphql';
+import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import projectPathQuery from '../queries/project_path.query.graphql';
import getRefMixin from './get_ref';
@@ -21,7 +22,7 @@ export default {
return this.$apollo
.query({
- query: filesQuery,
+ query: gon.features.paginatedTreeGraphqlQuery ? paginatedTreeQuery : filesQuery,
variables: {
projectPath: this.projectPath,
ref: this.ref,
diff --git a/app/assets/javascripts/repository/pages/blob.vue b/app/assets/javascripts/repository/pages/blob.vue
index 2645b294096..c09e2133936 100644
--- a/app/assets/javascripts/repository/pages/blob.vue
+++ b/app/assets/javascripts/repository/pages/blob.vue
@@ -3,11 +3,25 @@
// https://gitlab.com/gitlab-org/gitlab/-/issues/323200
import BlobContentViewer from '../components/blob_content_viewer.vue';
+import { LIMITED_CONTAINER_WIDTH_CLASS } from '../constants';
export default {
components: {
BlobContentViewer,
},
+ beforeRouteEnter(to, from, next) {
+ next(({ $options }) => {
+ $options.limitedContainerElements.forEach((el) =>
+ el.classList.remove(LIMITED_CONTAINER_WIDTH_CLASS),
+ );
+ });
+ },
+ beforeRouteLeave(to, from, next) {
+ this.$options.limitedContainerElements.forEach((el) =>
+ el.classList.add(LIMITED_CONTAINER_WIDTH_CLASS),
+ );
+ next();
+ },
props: {
path: {
type: String,
@@ -18,6 +32,7 @@ export default {
required: true,
},
},
+ limitedContainerElements: document.querySelectorAll(`.${LIMITED_CONTAINER_WIDTH_CLASS}`),
};
</script>
diff --git a/app/assets/javascripts/rest_api.js b/app/assets/javascripts/rest_api.js
index 3e9e3e6f265..61fe89f4f7e 100644
--- a/app/assets/javascripts/rest_api.js
+++ b/app/assets/javascripts/rest_api.js
@@ -4,7 +4,7 @@ export * from './api/user_api';
export * from './api/markdown_api';
// Note: It's not possible to spy on methods imported from this file in
-// Jest tests. See https://stackoverflow.com/a/53307822/1063392.
+// Jest tests.
// As a workaround, in Jest tests, import the methods from the file
// in which they are defined:
//
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 36f5e6f4ce1..23254fcc2eb 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
-import { hide } from '~/tooltips';
+import { hide, fixTitle } from '~/tooltips';
import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
@@ -75,6 +75,9 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
}
$this.attr('data-original-title', tooltipLabel);
+ $this.attr('title', tooltipLabel);
+ fixTitle($this);
+ hide($this);
if (!triggered) {
Cookies.set('collapsed_gutter', $('.right-sidebar').hasClass('right-sidebar-collapsed'));
@@ -99,7 +102,7 @@ Sidebar.prototype.toggleTodo = function (e) {
})
.catch(() =>
createFlash({
- message: sprintf(__('There was an error %{message} todo.'), {
+ message: sprintf(__('There was an error %{message} to-do item.'), {
message:
ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'),
}),
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 23ecee449a4..fedd2519958 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -2,12 +2,16 @@
import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
+import { formatNumber, sprintf, __ } from '~/locale';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
-import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
+import { statusTokenConfig } from '../components/search_tokens/status_token_config';
+import { tagTokenConfig } from '../components/search_tokens/tag_token_config';
+import { typeTokenConfig } from '../components/search_tokens/type_token_config';
+import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
import {
fromUrlQueryToSearch,
@@ -78,6 +82,21 @@ export default {
noRunnersFound() {
return !this.runnersLoading && !this.runners.items.length;
},
+ activeRunnersMessage() {
+ return sprintf(__('Runners currently online: %{active_runners_count}'), {
+ active_runners_count: formatNumber(this.activeRunnersCount),
+ });
+ },
+ searchTokens() {
+ return [
+ statusTokenConfig,
+ typeTokenConfig,
+ {
+ ...tagTokenConfig,
+ recentTokenValuesStorageKey: `${this.$options.filteredSearchNamespace}-recent-tags`,
+ },
+ ];
+ },
},
watch: {
search: {
@@ -99,6 +118,7 @@ export default {
captureException({ error, component: this.$options.name });
},
},
+ filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE,
INSTANCE_TYPE,
};
</script>
@@ -118,9 +138,13 @@ export default {
<runner-filtered-search-bar
v-model="search"
- namespace="admin_runners"
- :active-runners-count="activeRunnersCount"
- />
+ :tokens="searchTokens"
+ :namespace="$options.filteredSearchNamespace"
+ >
+ <template #runner-count>
+ {{ activeRunnersMessage }}
+ </template>
+ </runner-filtered-search-bar>
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
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 e14b3b17fa8..e04ca8ddca0 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -1,27 +1,8 @@
<script>
import { cloneDeep } from 'lodash';
-import { formatNumber, sprintf, __, s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { __ } from '~/locale';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import {
- STATUS_ACTIVE,
- STATUS_PAUSED,
- STATUS_ONLINE,
- STATUS_OFFLINE,
- STATUS_NOT_CONNECTED,
- INSTANCE_TYPE,
- GROUP_TYPE,
- PROJECT_TYPE,
- CREATED_DESC,
- CREATED_ASC,
- CONTACTED_DESC,
- CONTACTED_ASC,
- PARAM_KEY_STATUS,
- PARAM_KEY_RUNNER_TYPE,
- PARAM_KEY_TAG,
-} from '../constants';
-import TagToken from './search_tokens/tag_token.vue';
+import { CREATED_DESC, CREATED_ASC, CONTACTED_DESC, CONTACTED_ASC } from '../constants';
const sortOptions = [
{
@@ -58,10 +39,6 @@ export default {
type: String,
required: true,
},
- activeRunnersCount: {
- type: Number,
- required: true,
- },
},
data() {
// filtered_search_bar_root.vue may mutate the inital
@@ -73,62 +50,6 @@ export default {
initialSortBy: sort,
};
},
- computed: {
- searchTokens() {
- return [
- {
- icon: 'status',
- title: __('Status'),
- type: PARAM_KEY_STATUS,
- token: BaseToken,
- unique: true,
- options: [
- { value: STATUS_ACTIVE, title: s__('Runners|Active') },
- { value: STATUS_PAUSED, title: s__('Runners|Paused') },
- { value: STATUS_ONLINE, title: s__('Runners|Online') },
- { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
-
- // Added extra quotes in this title to avoid splitting this value:
- // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
- { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
- ],
- // TODO In principle we could support more complex search rules,
- // this can be added to a separate issue.
- operators: OPERATOR_IS_ONLY,
- },
-
- {
- icon: 'file-tree',
- title: __('Type'),
- type: PARAM_KEY_RUNNER_TYPE,
- token: BaseToken,
- unique: true,
- options: [
- { value: INSTANCE_TYPE, title: s__('Runners|instance') },
- { value: GROUP_TYPE, title: s__('Runners|group') },
- { value: PROJECT_TYPE, title: s__('Runners|project') },
- ],
- // TODO We should support more complex search rules,
- // search for multiple states (OR) or have NOT operators
- operators: OPERATOR_IS_ONLY,
- },
-
- {
- icon: 'tag',
- title: s__('Runners|Tags'),
- type: PARAM_KEY_TAG,
- token: TagToken,
- recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
- operators: OPERATOR_IS_ONLY,
- },
- ];
- },
- activeRunnersMessage() {
- return sprintf(__('Runners currently online: %{active_runners_count}'), {
- active_runners_count: formatNumber(this.activeRunnersCount),
- });
- },
- },
methods: {
onFilter(filters) {
const { sort } = this.value;
@@ -161,12 +82,13 @@ export default {
:sort-options="$options.sortOptions"
:initial-filter-value="initialFilterValue"
:initial-sort-by="initialSortBy"
- :tokens="searchTokens"
:search-input-placeholder="__('Search or filter results...')"
data-testid="runners-filtered-search"
@onFilter="onFilter"
@onSort="onSort"
/>
- <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div>
+ <div class="gl-text-right" data-testid="runner-count">
+ <slot name="runner-count"></slot>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index a5bc1680852..9a6fc07f6dd 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -135,9 +135,9 @@ export default {
</gl-form-checkbox>
<gl-form-checkbox
+ v-if="canBeLockedToProject"
v-model="model.locked"
data-testid="runner-field-locked"
- :disabled="!canBeLockedToProject"
>
{{ __('Lock to current projects') }}
<template #help>
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
new file mode 100644
index 00000000000..03dff5e61a5
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
@@ -0,0 +1,32 @@
+import { __, s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import {
+ STATUS_ACTIVE,
+ STATUS_PAUSED,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_NOT_CONNECTED,
+ PARAM_KEY_STATUS,
+} from '../../constants';
+
+export const statusTokenConfig = {
+ icon: 'status',
+ title: __('Status'),
+ type: PARAM_KEY_STATUS,
+ token: BaseToken,
+ unique: true,
+ options: [
+ { value: STATUS_ACTIVE, title: s__('Runners|Active') },
+ { value: STATUS_PAUSED, title: s__('Runners|Paused') },
+ { value: STATUS_ONLINE, title: s__('Runners|Online') },
+ { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
+
+ // Added extra quotes in this title to avoid splitting this value:
+ // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
+ { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
+ ],
+ // TODO In principle we could support more complex search rules,
+ // this can be added to a separate issue.
+ operators: OPERATOR_IS_ONLY,
+};
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 51fae60b6b7..ab67ac608e2 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -33,6 +33,7 @@ export default {
// 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, {
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js b/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js
new file mode 100644
index 00000000000..fdeba714385
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token_config.js
@@ -0,0 +1,12 @@
+import { s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { PARAM_KEY_TAG } from '../../constants';
+import TagToken from './tag_token.vue';
+
+export const tagTokenConfig = {
+ icon: 'tag',
+ title: s__('Runners|Tags'),
+ type: PARAM_KEY_TAG,
+ token: TagToken,
+ operators: OPERATOR_IS_ONLY,
+};
diff --git a/app/assets/javascripts/runner/components/search_tokens/type_token_config.js b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
new file mode 100644
index 00000000000..1da61c53386
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/type_token_config.js
@@ -0,0 +1,20 @@
+import { __, s__ } from '~/locale';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE, PARAM_KEY_RUNNER_TYPE } from '../../constants';
+
+export const typeTokenConfig = {
+ icon: 'file-tree',
+ title: __('Type'),
+ type: PARAM_KEY_RUNNER_TYPE,
+ token: BaseToken,
+ unique: true,
+ options: [
+ { value: INSTANCE_TYPE, title: s__('Runners|instance') },
+ { value: GROUP_TYPE, title: s__('Runners|group') },
+ { value: PROJECT_TYPE, title: s__('Runners|project') },
+ ],
+ // TODO We should support more complex search rules,
+ // search for multiple states (OR) or have NOT operators
+ operators: OPERATOR_IS_ONLY,
+};
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 2822882e0cc..46e55b322c7 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -2,6 +2,7 @@ import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
+export const GROUP_RUNNER_COUNT_LIMIT = 1000;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
@@ -50,3 +51,8 @@ export const CONTACTED_DESC = 'CONTACTED_DESC'; // TODO Add this to the API
export const CONTACTED_ASC = 'CONTACTED_ASC';
export const DEFAULT_SORT = CREATED_DESC;
+
+// Local storage namespaces
+
+export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners';
+export const GROUP_FILTERED_SEARCH_NAMESPACE = 'group_runners';
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
new file mode 100644
index 00000000000..a601ee8d611
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
@@ -0,0 +1,35 @@
+#import "~/runner/graphql/runner_node.fragment.graphql"
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getGroupRunners(
+ $groupFullPath: ID!
+ $before: String
+ $after: String
+ $first: Int
+ $last: Int
+ $status: CiRunnerStatus
+ $type: CiRunnerType
+ $search: String
+ $sort: CiRunnerSort
+) {
+ group(fullPath: $groupFullPath) {
+ runners(
+ membership: DESCENDANTS
+ before: $before
+ after: $after
+ first: $first
+ last: $last
+ status: $status
+ type: $type
+ search: $search
+ sort: $sort
+ ) {
+ nodes {
+ ...RunnerNode
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
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 07bbf60c453..42e1a9e1de9 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -1,18 +1,135 @@
<script>
+import createFlash from '~/flash';
+import { fetchPolicies } from '~/lib/graphql';
+import { updateHistory } from '~/lib/utils/url_utility';
+import { formatNumber, sprintf, s__ } from '~/locale';
+import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
+import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
+import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
-import { GROUP_TYPE } from '../constants';
+import { statusTokenConfig } from '../components/search_tokens/status_token_config';
+import { typeTokenConfig } from '../components/search_tokens/type_token_config';
+import {
+ I18N_FETCH_ERROR,
+ GROUP_FILTERED_SEARCH_NAMESPACE,
+ GROUP_TYPE,
+ GROUP_RUNNER_COUNT_LIMIT,
+} from '../constants';
+import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
+import {
+ fromUrlQueryToSearch,
+ fromSearchToUrl,
+ fromSearchToVariables,
+} from '../runner_search_utils';
+import { captureException } from '../sentry_utils';
export default {
+ name: 'GroupRunnersApp',
components: {
+ RunnerFilteredSearchBar,
+ RunnerList,
RunnerManualSetupHelp,
RunnerTypeHelp,
+ RunnerPagination,
},
props: {
registrationToken: {
type: String,
required: true,
},
+ groupFullPath: {
+ type: String,
+ required: true,
+ },
+ groupRunnersLimitedCount: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: fromUrlQueryToSearch(),
+ runners: {
+ items: [],
+ pageInfo: {},
+ },
+ };
+ },
+ apollo: {
+ runners: {
+ query: getGroupRunnersQuery,
+ // Runners can be updated by users directly in this list.
+ // A "cache and network" policy prevents outdated filtered
+ // results.
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ variables() {
+ return this.variables;
+ },
+ update(data) {
+ const { runners } = data?.group || {};
+ return {
+ items: runners?.nodes || [],
+ pageInfo: runners?.pageInfo || {},
+ };
+ },
+ error(error) {
+ createFlash({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
+ },
+ },
+ },
+ computed: {
+ variables() {
+ return {
+ ...fromSearchToVariables(this.search),
+ groupFullPath: this.groupFullPath,
+ };
+ },
+ runnersLoading() {
+ return this.$apollo.queries.runners.loading;
+ },
+ noRunnersFound() {
+ return !this.runnersLoading && !this.runners.items.length;
+ },
+ groupRunnersCount() {
+ if (this.groupRunnersLimitedCount > GROUP_RUNNER_COUNT_LIMIT) {
+ return `${formatNumber(GROUP_RUNNER_COUNT_LIMIT)}+`;
+ }
+ return formatNumber(this.groupRunnersLimitedCount);
+ },
+ runnerCountMessage() {
+ return sprintf(s__('Runners|Runners in this group: %{groupRunnersCount}'), {
+ groupRunnersCount: this.groupRunnersCount,
+ });
+ },
+ searchTokens() {
+ return [statusTokenConfig, typeTokenConfig];
+ },
+ filteredSearchNamespace() {
+ return `${GROUP_FILTERED_SEARCH_NAMESPACE}/${this.groupFullPath}`;
+ },
+ },
+ watch: {
+ search: {
+ deep: true,
+ handler() {
+ // TODO Implement back button reponse using onpopstate
+ updateHistory({
+ url: fromSearchToUrl(this.search),
+ title: document.title,
+ });
+ },
+ },
+ },
+ errorCaptured(error) {
+ this.reportToSentry(error);
+ },
+ methods: {
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
},
GROUP_TYPE,
};
@@ -31,5 +148,23 @@ export default {
/>
</div>
</div>
+
+ <runner-filtered-search-bar
+ v-model="search"
+ :tokens="searchTokens"
+ :namespace="filteredSearchNamespace"
+ >
+ <template #runner-count>
+ {{ runnerCountMessage }}
+ </template>
+ </runner-filtered-search-bar>
+
+ <div v-if="noRunnersFound" class="gl-text-center gl-p-5">
+ {{ __('No runners found') }}
+ </div>
+ <template v-else>
+ <runner-list :runners="runners.items" :loading="runnersLoading" />
+ <runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/runner/group_runners/index.js b/app/assets/javascripts/runner/group_runners/index.js
index e14c583d73e..9545764c68d 100644
--- a/app/assets/javascripts/runner/group_runners/index.js
+++ b/app/assets/javascripts/runner/group_runners/index.js
@@ -12,7 +12,13 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
return null;
}
- const { registrationToken, groupId } = el.dataset;
+ const {
+ registrationToken,
+ runnerInstallHelpPage,
+ groupId,
+ groupFullPath,
+ groupRunnersLimitedCount,
+ } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
@@ -27,12 +33,15 @@ export const initGroupRunners = (selector = '#js-group-runners') => {
el,
apolloProvider,
provide: {
+ runnerInstallHelpPage,
groupId,
},
render(h) {
return h(GroupRunnersApp, {
props: {
registrationToken,
+ groupFullPath,
+ groupRunnersLimitedCount: parseInt(groupRunnersLimitedCount, 10),
},
});
},
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index 65f75eb11ac..0a817ea0acf 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -43,7 +43,6 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
urlQueryToFilter(query, {
filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
- legacySpacesDecode: false,
}),
),
sort: params[PARAM_KEY_SORT] || DEFAULT_SORT,
diff --git a/app/assets/javascripts/search/highlight_blob_search_result.js b/app/assets/javascripts/search/highlight_blob_search_result.js
index c553d5b14a0..07967434f37 100644
--- a/app/assets/javascripts/search/highlight_blob_search_result.js
+++ b/app/assets/javascripts/search/highlight_blob_search_result.js
@@ -2,7 +2,7 @@ export default (search = '') => {
const highlightLineClass = 'hll';
const contentBody = document.getElementById('content-body');
const searchTerm = search.toLowerCase();
- const blobs = contentBody.querySelectorAll('.blob-result');
+ const blobs = contentBody.querySelectorAll('.js-blob-result');
blobs.forEach((blob) => {
const lines = blob.querySelectorAll('.line');
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index ee5e778f63d..be64a9278e3 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -40,7 +40,7 @@ export const fetchProjects = ({ commit, state }, search) => {
);
} else {
// The .catch() is due to the API method not handling a rejection properly
- Api.projects(search, { order_by: 'id' }, callback).catch(() => {
+ Api.projects(search, { order_by: 'similarity' }, callback).catch(() => {
callback();
});
}
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
index b7d97213594..b00b9bb0f2e 100644
--- a/app/assets/javascripts/search/store/utils.js
+++ b/app/assets/javascripts/search/store/utils.js
@@ -6,7 +6,7 @@ function extractKeys(object, keyList) {
}
export const loadDataFromLS = (key) => {
- if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (!AccessorUtilities.canUseLocalStorage()) {
return [];
}
@@ -20,7 +20,7 @@ export const loadDataFromLS = (key) => {
};
export const setFrequentItemToLS = (key, data, itemData) => {
- if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (!AccessorUtilities.canUseLocalStorage()) {
return [];
}
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index ebe0138f046..6a282df99bf 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -10,6 +10,7 @@ import {
REPORT_TYPE_CONTAINER_SCANNING,
REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_CORPUS_MANAGEMENT,
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
@@ -104,6 +105,12 @@ export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath(
{ anchor: 'configuration' },
);
+export const CORPUS_MANAGEMENT_NAME = __('Corpus Management');
+export const CORPUS_MANAGEMENT_DESCRIPTION = s__(
+ 'SecurityConfiguration|Manage corpus files used as mutation sources in coverage fuzzing.',
+);
+export const CORPUS_MANAGEMENT_CONFIG_TEXT = s__('SecurityConfiguration|Manage corpus');
+
export const API_FUZZING_NAME = __('API Fuzzing');
export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.');
export const API_FUZZING_HELP_PATH = helpPagePath('user/application_security/api_fuzzing/index');
@@ -202,6 +209,14 @@ export const securityFeatures = [
helpPath: COVERAGE_FUZZING_HELP_PATH,
configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH,
type: REPORT_TYPE_COVERAGE_FUZZING,
+ secondary: gon?.features?.corpusManagement
+ ? {
+ type: REPORT_TYPE_CORPUS_MANAGEMENT,
+ name: CORPUS_MANAGEMENT_NAME,
+ description: CORPUS_MANAGEMENT_DESCRIPTION,
+ configurationText: CORPUS_MANAGEMENT_CONFIG_TEXT,
+ }
+ : {},
},
];
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index a3a2c794a67..8f3c4c644bf 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -19,7 +19,6 @@ const IGNORE_ERRORS = [
'fb_xd_fragment',
// ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
// reduce this. (thanks @acdha)
- // See http://stackoverflow.com/questions/4113268
'bmi_SafeAddOnload',
'EBCallBackMessageReceived',
// See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
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 e522e3ff408..b1c8f6ef22e 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,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import {
GlToast,
GlModal,
@@ -8,6 +7,7 @@ import {
GlFormCheckbox,
GlDropdown,
GlDropdownItem,
+ GlSafeHtmlDirective,
} from '@gitlab/ui';
import $ from 'jquery';
import Vue from 'vue';
@@ -49,6 +49,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -234,6 +235,7 @@ export default {
},
},
statusTimeRanges,
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
@@ -267,7 +269,7 @@ export default {
@click="setEmoji"
>
<template #button-content>
- <span v-html="emojiTag"></span>
+ <span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
@@ -289,7 +291,7 @@ export default {
class="js-toggle-emoji-menu emoji-menu-toggle-button btn"
@click="showEmojiMenu"
>
- <span v-html="emojiTag"></span>
+ <span v-safe-html:[$options.safeHtmlConfig]="emojiTag"></span>
<span
v-show="noEmoji"
class="js-no-emoji-placeholder no-emoji-placeholder position-relative"
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index d9c5edc91f1..f98aa0dc77d 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -53,7 +53,7 @@ export default {
class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
data-test-id="edit-link"
- data-track-event="click_edit_button"
+ data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
>
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 1dd05d3886e..1b28ba2afd1 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -3,7 +3,6 @@ import { GlDropdownItem } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import createFlash from '~/flash';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import { __, n__ } from '~/locale';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
@@ -173,7 +172,7 @@ export default {
})
.then(({ data }) => {
this.$emit('assignees-updated', {
- id: getIdFromGraphQLId(data.issuableSetAssignees.issuable.id),
+ id: data.issuableSetAssignees.issuable.id,
assignees: data.issuableSetAssignees.issuable.assignees.nodes,
});
return data;
diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
index 55179947756..9fdf941579d 100644
--- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
+++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue
@@ -144,16 +144,11 @@ export default {
v-if="glFeatures.labelsWidget"
class="block labels js-labels-block"
:allow-label-remove="allowLabelEdit"
- :allow-label-create="allowLabelCreate"
- :allow-label-edit="allowLabelEdit"
:allow-multiselect="true"
- :allow-scoped-labels="allowScopedLabels"
:footer-create-label-title="__('Create project label')"
:footer-manage-label-title="__('Manage project labels')"
:labels-create-title="__('Create project label')"
- :labels-fetch-path="labelsFetchPath"
:labels-filter-base-path="projectIssuesPath"
- :labels-manage-path="labelsManagePath"
:labels-select-in-progress="isLabelsSelectInProgress"
:selected-labels="selectedLabels"
:variant="$options.variant"
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 19543d0927a..cb49f329f7e 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -93,7 +93,7 @@ export default {
class="float-right lock-edit btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary gl-mr-n2"
href="#"
data-testid="edit-link"
- data-track-event="click_edit_button"
+ data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="lock_issue"
@click.prevent="toggleForm"
diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
index 39f72b251c7..a09138a708b 100644
--- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
+++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue
@@ -56,6 +56,11 @@ export default {
return this.$apollo.queries.participants.loading;
},
},
+ methods: {
+ toggleSidebar() {
+ this.$emit('toggleSidebar');
+ },
+ },
};
</script>
@@ -66,5 +71,6 @@ export default {
:number-of-less-participants="7"
:lazy="false"
class="block participants"
+ @toggleSidebar="toggleSidebar"
/>
</template>
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 1243603805a..367dcdb961b 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -40,7 +40,7 @@ export default {
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#"
- data-track-event="click_edit_button"
+ data-track-action="click_edit_button"
data-track-label="right_sidebar"
data-track-property="reviewer"
>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 8ccc0102c3d..8f4d5406da8 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -369,6 +369,7 @@ export default {
:text="dropdownText"
:loading="loading"
class="gl-w-full"
+ toggle-class="gl-max-w-100"
@shown="setFocus"
>
<gl-search-box-by-type ref="search" v-model="searchTerm" />
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 89aa03fd954..22adbd79ef6 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -136,7 +136,7 @@ export default {
size="small"
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2"
data-testid="edit-button"
- :data-track-event="tracking.event"
+ :data-track-action="tracking.event"
:data-track-label="tracking.label"
:data-track-property="tracking.property"
data-qa-selector="edit_link"
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
index 33c6ac6e2ba..db2197ec65e 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { sprintf, s__ } from '~/locale';
export default {
@@ -27,5 +26,5 @@ export default {
</script>
<template>
- <div data-testid="spentOnlyPane" v-html="timeSpent"></div>
+ <div data-testid="spentOnlyPane" v-html="timeSpent /* eslint-disable-line vue/no-v-html */"></div>
</template>
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 031472a7d20..10ab80f4ec2 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,7 +1,6 @@
import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import createFlash from '~/flash';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
@@ -13,7 +12,6 @@ import {
isInIncidentPage,
parseBoolean,
} from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
@@ -258,6 +256,8 @@ export function mountSidebarLabels() {
allowScopedLabels: parseBoolean(el.dataset.allowScopedLabels),
initiallySelectedLabels: JSON.parse(el.dataset.selectedLabels),
variant: DropdownVariant.Sidebar,
+ canUpdate: parseBoolean(el.dataset.canEdit),
+ isClassicSidebar: true,
},
render: (createElement) => createElement(SidebarLabels),
});
@@ -361,10 +361,10 @@ function mountReferenceComponent() {
});
}
-function mountLockComponent() {
+function mountLockComponent(store) {
const el = document.getElementById('js-lock-entry-point');
- if (!el) {
+ if (!el || !store) {
return;
}
@@ -373,37 +373,20 @@ function mountLockComponent() {
const dataNode = document.getElementById('js-lock-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
- let importStore;
- if (isInIssuePage() || isInIncidentPage()) {
- importStore = import(/* webpackChunkName: 'notesStore' */ '~/notes/stores').then(
- ({ store }) => store,
- );
- } else {
- importStore = import(/* webpackChunkName: 'mrNotesStore' */ '~/mr_notes/stores').then(
- (store) => store.default,
- );
- }
-
- importStore
- .then(
- (store) =>
- new Vue({
- el,
- store,
- provide: {
- fullPath,
- },
- render: (createElement) =>
- createElement(IssuableLockForm, {
- props: {
- isEditable: initialData.is_editable,
- },
- }),
- }),
- )
- .catch(() => {
- createFlash({ message: __('Failed to load sidebar lock status') });
- });
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ store,
+ provide: {
+ fullPath,
+ },
+ render: (createElement) =>
+ createElement(IssuableLockForm, {
+ props: {
+ isEditable: initialData.is_editable,
+ },
+ }),
+ });
}
function mountParticipantsComponent() {
@@ -535,7 +518,7 @@ function mountCopyEmailComponent() {
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage()) && gon.features.issueAssigneesWidget;
-export function mountSidebar(mediator) {
+export function mountSidebar(mediator, store) {
initInviteMembersModal();
initInviteMembersTrigger();
@@ -546,11 +529,12 @@ export function mountSidebar(mediator) {
mountAssigneesComponentDeprecated(mediator);
}
mountReviewersComponent(mediator);
+ mountSidebarLabels();
mountMilestoneSelect();
mountConfidentialComponent(mediator);
mountDueDateComponent(mediator);
mountReferenceComponent(mediator);
- mountLockComponent();
+ mountLockComponent(store);
mountParticipantsComponent();
mountSubscriptionsComponent();
mountCopyEmailComponent();
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index ace2a163adc..cea26acd101 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -22,7 +22,6 @@ export default class SidebarService {
constructor(endpointMap) {
if (!SidebarService.singleton) {
this.endpoint = endpointMap.endpoint;
- this.toggleSubscriptionEndpoint = endpointMap.toggleSubscriptionEndpoint;
this.moveIssueEndpoint = endpointMap.moveIssueEndpoint;
this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint;
this.fullPath = endpointMap.fullPath;
@@ -75,10 +74,6 @@ export default class SidebarService {
});
}
- toggleSubscription() {
- return axios.post(this.toggleSubscriptionEndpoint);
- }
-
moveIssue(moveToProjectId) {
return axios.post(this.moveIssueEndpoint, {
move_to_project_id: moveToProjectId,
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 063e3313a3c..1be670f7590 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,9 +1,9 @@
-import { mountSidebar, getSidebarOptions } from './mount_sidebar';
+import { mountSidebar, getSidebarOptions } from 'ee_else_ce/sidebar/mount_sidebar';
import Mediator from './sidebar_mediator';
-export default () => {
+export default (store) => {
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
- mountSidebar(mediator);
+ mountSidebar(mediator, store);
};
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 0a5e44a9b95..9144e3b08db 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -17,7 +17,6 @@ export default class SidebarMediator {
this.store = new Store(options);
this.service = new Service({
endpoint: options.endpoint,
- toggleSubscriptionEndpoint: options.toggleSubscriptionEndpoint,
moveIssueEndpoint: options.moveIssueEndpoint,
projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint,
fullPath: options.fullPath,
@@ -85,22 +84,6 @@ export default class SidebarMediator {
this.store.setAssigneeData(data);
this.store.setReviewerData(data);
this.store.setTimeTrackingData(data);
- this.store.setParticipantsData(data);
- this.store.setSubscriptionsData(data);
- }
-
- toggleSubscription() {
- this.store.setFetchingState('subscriptions', true);
- return this.service
- .toggleSubscription()
- .then(() => {
- this.store.setSubscribedState(!this.store.subscribed);
- this.store.setFetchingState('subscriptions', false);
- })
- .catch((err) => {
- this.store.setFetchingState('subscriptions', false);
- throw err;
- });
}
fetchAutocompleteProjects(searchTerm) {
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 3c108b06eab..94c54fc0980 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -22,8 +22,6 @@ export default class SidebarStore {
this.isFetching = {
assignees: true,
reviewers: true,
- participants: true,
- subscriptions: true,
};
this.isLoading = {};
this.autocompleteProjects = [];
@@ -63,18 +61,6 @@ export default class SidebarStore {
this.humanTotalTimeSpent = data.human_total_time_spent;
}
- setParticipantsData(data) {
- this.isFetching.participants = false;
- this.participants = data.participants || [];
- }
-
- setSubscriptionsData(data) {
- this.projectEmailsDisabled = data.project_emails_disabled || false;
- this.subscribeDisabledDescription = data.subscribe_disabled_description;
- this.isFetching.subscriptions = false;
- this.subscribed = data.subscribed || false;
- }
-
setFetchingState(key, value) {
this.isFetching[key] = value;
}
diff --git a/app/assets/javascripts/sidebar/track_invite_members.js b/app/assets/javascripts/sidebar/track_invite_members.js
index eab15578f0f..45a3366197b 100644
--- a/app/assets/javascripts/sidebar/track_invite_members.js
+++ b/app/assets/javascripts/sidebar/track_invite_members.js
@@ -2,10 +2,12 @@ import $ from 'jquery';
import Tracking from '~/tracking';
export default function initTrackInviteMembers(userDropdown) {
- const { trackEvent, trackLabel } = userDropdown.querySelector('.js-invite-members-track').dataset;
+ const { trackAction, trackLabel } = userDropdown.querySelector(
+ '.js-invite-members-track',
+ ).dataset;
$(userDropdown).on('shown.bs.dropdown', () => {
- Tracking.event(undefined, trackEvent, {
+ Tracking.event(undefined, trackAction, {
label: trackLabel,
});
});
diff --git a/app/assets/javascripts/snippet/snippet_show.js b/app/assets/javascripts/snippet/snippet_show.js
index 22dffa90cef..6d0e4770e1c 100644
--- a/app/assets/javascripts/snippet/snippet_show.js
+++ b/app/assets/javascripts/snippet/snippet_show.js
@@ -1,12 +1,12 @@
import loadAwardsHandler from '~/awards_handler';
-import initNotes from '~/init_notes';
+import initDeprecatedNotes from '~/init_deprecated_notes';
import SnippetsAppFactory from '~/snippets';
import SnippetsShow from '~/snippets/components/show.vue';
import ZenMode from '~/zen_mode';
SnippetsAppFactory(document.getElementById('js-snippet-view'), SnippetsShow);
-initNotes();
+initDeprecatedNotes();
loadAwardsHandler();
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index e462f20535b..62d95a650da 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
export default {
@@ -17,6 +16,9 @@ export default {
</script>
<template>
<markdown-field-view class="snippet-description" data-qa-selector="snippet_description_content">
- <div class="md js-snippet-description" v-html="description"></div>
+ <div
+ class="md js-snippet-description"
+ v-html="description /* eslint-disable-line vue/no-v-html */"
+ ></div>
</markdown-field-view>
</template>
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js
index d770dd18d7f..e41dc51457a 100644
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/services/renderers/render_identifier_instance_text.js
@@ -18,7 +18,7 @@ Regexp notes:
const identifierInstanceRegex = /((?:\[.+?\]){1}(?:\[\]|\[.+?\])?(?!:))/g;
const isIdentifierInstance = (literal) => {
- // Reset lastIndex as global flag in regexp are stateful (https://stackoverflow.com/a/11477448)
+ // Reset lastIndex as global flag in regexp are stateful
identifierInstanceRegex.lastIndex = 0;
return identifierInstanceRegex.test(literal);
};
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index 598111e4086..062a3404355 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -24,3 +24,7 @@ export const LOAD_ACTION_ATTR_SELECTOR = '[data-track-action="render"]';
export const DEPRECATED_EVENT_ATTR_SELECTOR = '[data-track-event]';
export const DEPRECATED_LOAD_EVENT_ATTR_SELECTOR = '[data-track-event="render"]';
+
+export const URLS_CACHE_STORAGE_KEY = 'gl-snowplow-pseudonymized-urls';
+
+export const REFERRER_TTL = 24 * 60 * 60 * 1000;
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index 5417e2d969b..7e99ecb4f4e 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -1,3 +1,4 @@
+import { getAllExperimentContexts } from '~/experimentation/utils';
import { DEFAULT_SNOWPLOW_OPTIONS } from './constants';
import getStandardContext from './get_standard_context';
import Tracking from './tracking';
@@ -38,10 +39,14 @@ export function initDefaultTrackers() {
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
+ // must be before initializing the trackers
+ Tracking.setAnonymousUrls();
+
window.snowplow('enableActivityTracking', 30, 30);
// must be after enableActivityTracking
const standardContext = getStandardContext();
- window.snowplow('trackPageView', null, [standardContext]);
+ const experimentContexts = getAllExperimentContexts();
+ window.snowplow('trackPageView', null, [standardContext, ...experimentContexts]);
if (window.snowplowOptions.formTracking) {
Tracking.enableFormTracking(opts.formTrackingConfig);
diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js
index a1f745bc172..657e0a79911 100644
--- a/app/assets/javascripts/tracking/tracking.js
+++ b/app/assets/javascripts/tracking/tracking.js
@@ -1,7 +1,14 @@
import { LOAD_ACTION_ATTR_SELECTOR, DEPRECATED_LOAD_EVENT_ATTR_SELECTOR } from './constants';
import { dispatchSnowplowEvent } from './dispatch_snowplow_event';
import getStandardContext from './get_standard_context';
-import { getEventHandlers, createEventPayload, renameKey, addExperimentContext } from './utils';
+import {
+ getEventHandlers,
+ createEventPayload,
+ renameKey,
+ addExperimentContext,
+ getReferrersCache,
+ addReferrersCacheEntry,
+} from './utils';
export default class Tracking {
static queuedEvents = [];
@@ -159,6 +166,37 @@ export default class Tracking {
}
/**
+ * Replaces the URL and referrer for the default web context
+ * if the replacements are available.
+ *
+ * @returns {undefined}
+ */
+ static setAnonymousUrls() {
+ const { snowplowPseudonymizedPageUrl: pageUrl } = window.gl;
+
+ if (!pageUrl) {
+ return;
+ }
+
+ const referrers = getReferrersCache();
+ const pageLinks = Object.seal({ url: '', referrer: '', originalUrl: window.location.href });
+
+ pageLinks.url = `${pageUrl}${window.location.hash}`;
+ window.snowplow('setCustomUrl', pageLinks.url);
+
+ if (document.referrer) {
+ const node = referrers.find((links) => links.originalUrl === document.referrer);
+
+ if (node) {
+ pageLinks.referrer = node.url;
+ window.snowplow('setReferrerUrl', pageLinks.referrer);
+ }
+ }
+
+ addReferrersCacheEntry(referrers, pageLinks);
+ }
+
+ /**
* Returns an implementation of this class in the form of
* a Vue mixin.
*
diff --git a/app/assets/javascripts/tracking/utils.js b/app/assets/javascripts/tracking/utils.js
index 1189b2168ad..3507872b511 100644
--- a/app/assets/javascripts/tracking/utils.js
+++ b/app/assets/javascripts/tracking/utils.js
@@ -6,6 +6,8 @@ import {
LOAD_ACTION_ATTR_SELECTOR,
DEPRECATED_EVENT_ATTR_SELECTOR,
DEPRECATED_LOAD_EVENT_ATTR_SELECTOR,
+ URLS_CACHE_STORAGE_KEY,
+ REFERRER_TTL,
} from './constants';
export const addExperimentContext = (opts) => {
@@ -100,3 +102,25 @@ export const renameKey = (o, oldKey, newKey) => {
return ret;
};
+
+export const filterOldReferrersCacheEntries = (cache) => {
+ const now = Date.now();
+
+ return cache.filter((entry) => entry.timestamp && entry.timestamp > now - REFERRER_TTL);
+};
+
+export const getReferrersCache = () => {
+ try {
+ const referrers = JSON.parse(window.localStorage.getItem(URLS_CACHE_STORAGE_KEY) || '[]');
+
+ return filterOldReferrersCacheEntries(referrers);
+ } catch {
+ return [];
+ }
+};
+
+export const addReferrersCacheEntry = (cache, entry) => {
+ const referrers = JSON.stringify([{ ...entry, timestamp: Date.now() }, ...cache]);
+
+ window.localStorage.setItem(URLS_CACHE_STORAGE_KEY, referrers);
+};
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 0e25f71fe05..7a7518bcf83 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -1,7 +1,4 @@
import Vue from 'vue';
-
-import { sanitize } from '~/lib/dompurify';
-
import UsersCache from './lib/utils/users_cache';
import UserPopover from './vue_shared/components/user_popover/user_popover.vue';
@@ -41,7 +38,6 @@ const populateUserInfo = (user) => {
name: userData.name,
location: userData.location,
bio: userData.bio,
- bioHtml: sanitize(userData.bio_html),
workInformation: userData.work_information,
websiteUrl: userData.website_url,
pronouns: userData.pronouns,
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index 7c17ce85cc6..69b3c27173f 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -536,9 +536,6 @@ function UsersSelect(currentUser, els, options = {}) {
opened(e) {
const $el = $(e.currentTarget);
const selected = getSelected();
- if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) {
- this.addInput($dropdown.data('fieldName'), 0, {});
- }
$el.find('.is-active').removeClass('is-active');
function highlightSelected(id) {
@@ -547,8 +544,6 @@ function UsersSelect(currentUser, els, options = {}) {
if (selected.length > 0) {
getSelected().forEach((selectedId) => highlightSelected(selectedId));
- } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
- highlightSelected(0);
} else {
highlightSelected(selectedId);
}
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 ac6368a3025..306026072a3 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
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { s__, n__ } from '~/locale';
export default {
@@ -32,11 +31,16 @@ export default {
</script>
<template>
<section class="mr-info-list gl-ml-7 gl-pb-5">
- <p v-if="relatedLinks.closing">{{ closesText }} <span v-html="relatedLinks.closing"></span></p>
+ <p v-if="relatedLinks.closing">
+ {{ closesText }}
+ <span v-html="relatedLinks.closing /* eslint-disable-line vue/no-v-html */"></span>
+ </p>
<p v-if="relatedLinks.mentioned">
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
- <span v-html="relatedLinks.mentioned"></span>
+ <span v-html="relatedLinks.mentioned /* eslint-disable-line vue/no-v-html */"></span>
+ </p>
+ <p v-if="relatedLinks.assignToMe">
+ <span v-html="relatedLinks.assignToMe /* eslint-disable-line vue/no-v-html */"></span>
</p>
- <p v-if="relatedLinks.assignToMe"><span v-html="relatedLinks.assignToMe"></span></p>
</section>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
index d2581f57837..f3673005c45 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue
@@ -98,7 +98,7 @@ export default {
data-testid="add-pipeline-link"
:data-track-property="humanAccess"
:data-track-value="$options.SP_LINK_TRACK_VALUE"
- :data-track-event="$options.SP_LINK_TRACK_EVENT"
+ :data-track-action="$options.SP_LINK_TRACK_EVENT"
:data-track-label="$options.SP_TRACK_LABEL"
>
{{ content }}
@@ -139,7 +139,7 @@ export default {
:href="pipelinePath"
:data-track-property="humanAccess"
:data-track-value="$options.SP_SHOW_TRACK_VALUE"
- :data-track-event="$options.SP_SHOW_TRACK_EVENT"
+ :data-track-action="$options.SP_SHOW_TRACK_EVENT"
:data-track-label="$options.SP_TRACK_LABEL"
>
{{ __('Show me how to add a pipeline') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
index ebd2b5cd22d..e31e69d0f3a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/review_app_link.vue
@@ -39,7 +39,7 @@ export default {
target="_blank"
rel="noopener noreferrer nofollow"
:class="cssClass"
- data-track-event="open_review_app"
+ data-track-action="open_review_app"
data-track-label="review_app"
>
{{ display.text }} <gl-icon class="fgray" name="external-link" />
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
index d331f1690f5..a55dba92e16 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commits_header.vue
@@ -1,5 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
+/* eslint-disable @gitlab/require-string-literal-i18n-helpers */
import { GlButton } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, n__, sprintf, s__ } from '~/locale';
@@ -89,7 +89,10 @@ export default {
/>
<span v-if="expanded">{{ __('Collapse') }}</span>
<span v-else>
- <span class="vertical-align-middle" v-html="message"></span>
+ <span
+ class="vertical-align-middle"
+ v-html="message /* eslint-disable-line vue/no-v-html */"
+ ></span>
<gl-button variant="link" class="modify-message-button">
{{ modifyLinkMessage }}
</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
new file mode 100644
index 00000000000..503ddf8a396
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import notesEventHub from '~/notes/event_hub';
+import StatusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ i18n: {
+ pipelineFailed: s__(
+ 'mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure.',
+ ),
+ approvalNeeded: s__('mrWidget|You can only merge once this merge request is approved.'),
+ unresolvedDiscussions: s__('mrWidget|Merge blocked: all threads must be resolved.'),
+ },
+ components: {
+ StatusIcon,
+ GlButton,
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ failedText() {
+ if (this.mr.isPipelineFailed) {
+ return this.$options.i18n.pipelineFailed;
+ } else if (this.mr.approvals && !this.mr.isApproved) {
+ return this.$options.i18n.approvalNeeded;
+ } else if (this.mr.hasMergeableDiscussionsState) {
+ return this.$options.i18n.unresolvedDiscussions;
+ }
+
+ return null;
+ },
+ },
+ methods: {
+ jumpToFirstUnresolvedDiscussion() {
+ notesEventHub.$emit('jumpToFirstUnresolvedDiscussion');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media gl-flex-wrap">
+ <status-icon status="warning" />
+ <p class="media-body gl-m-0! gl-font-weight-bold gl-text-black-normal!">
+ {{ failedText }}
+ <template v-if="failedText == $options.i18n.unresolvedDiscussions">
+ <gl-button
+ class="gl-ml-3"
+ size="small"
+ variant="confirm"
+ data-testid="jumpToUnresolved"
+ @click="jumpToFirstUnresolvedDiscussion"
+ >
+ {{ s__('mrWidget|Jump to first unresolved thread') }}
+ </gl-button>
+ <gl-button
+ v-if="mr.createIssueToResolveDiscussionsPath"
+ :href="mr.createIssueToResolveDiscussionsPath"
+ class="gl-ml-3"
+ size="small"
+ variant="confirm"
+ category="secondary"
+ data-testid="resolveIssue"
+ >
+ {{ s__('mrWidget|Create issue to resolve all threads') }}
+ </gl-button>
+ </template>
+ </p>
+ </div>
+</template>
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 22f41b43095..1976d3639a6 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,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { escape } from 'lodash';
import createFlash from '~/flash';
@@ -171,7 +170,7 @@ export default {
v-if="!rebaseInProgress && !canPushToSourceBranch"
class="gl-font-weight-bold gl-ml-0!"
data-testid="rebase-message"
- v-html="fastForwardMergeText"
+ v-html="fastForwardMergeText /* eslint-disable-line vue/no-v-html */"
></span>
<div
v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
new file mode 100644
index 00000000000..9a7743348ff
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
@@ -0,0 +1,49 @@
+<script>
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import readyToMergeQuery from '../../queries/states/new_ready_to_merge.query.graphql';
+import StatusIcon from '../mr_widget_status_icon.vue';
+
+export default {
+ apollo: {
+ canMerge: {
+ query: readyToMergeQuery,
+ skip() {
+ return !this.mr || !window.gon?.features?.mergeRequestWidgetGraphql;
+ },
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest?.userPermissions?.canMerge,
+ },
+ },
+ components: {
+ StatusIcon,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ props: {
+ mr: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ canMerge: null,
+ };
+ },
+};
+</script>
+
+<template>
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <p class="media-body gl-m-0! gl-font-weight-bold">
+ <template v-if="canMerge">
+ {{ __('Ready to merge!') }}
+ </template>
+ <template v-else>
+ {{ __('Ready to merge by members who can write to the target branch.') }}
+ </template>
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 01e0b91bd4a..7827c79cd31 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -11,6 +10,9 @@ export default {
GlSprintf,
GlLink,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
props: {
mr: {
type: Object,
@@ -21,6 +23,7 @@ export default {
return { emptyStateSVG };
},
ciHelpPage: helpPagePath('/ci/quick_start/index.html'),
+ safeHtmlConfig: { ADD_TAGS: ['use'] },
};
</script>
@@ -30,7 +33,7 @@ export default {
<div
class="artwork col-md-5 order-md-last col-12 text-center d-flex justify-content-center align-items-center"
>
- <span v-html="emptyStateSVG"></span>
+ <span v-safe-html:[$options.safeHtmlConfig]="emptyStateSVG"></span>
</div>
<div class="text col-md-7 order-md-first col-12">
<p class="highlight">
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 f33f4d3fda0..7df65e995a5 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
@@ -28,6 +28,7 @@ import {
CONFIRM,
WARNING,
MT_MERGE_STRATEGY,
+ PIPELINE_FAILED_STATE,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -39,7 +40,6 @@ import CommitsHeader from './commits_header.vue';
import SquashBeforeMerge from './squash_before_merge.vue';
const PIPELINE_RUNNING_STATE = 'running';
-const PIPELINE_FAILED_STATE = 'failed';
const PIPELINE_PENDING_STATE = 'pending';
const PIPELINE_SUCCESS_STATE = 'success';
@@ -105,6 +105,10 @@ export default {
import(
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
),
+ MergeTrainFailedPipelineConfirmationDialog: () =>
+ import(
+ 'ee_component/vue_merge_request_widget/components/merge_train_failed_pipeline_confirmation_dialog.vue'
+ ),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -125,6 +129,7 @@ export default {
squashBeforeMerge: this.mr.squashIsSelected,
isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage,
+ isPipelineFailedModalVisible: false,
};
},
computed: {
@@ -327,7 +332,12 @@ export default {
: this.mr.commitMessageWithDescription;
this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
},
- handleMergeButtonClick(useAutoMerge, mergeImmediately = false) {
+ handleMergeButtonClick(useAutoMerge, mergeImmediately = false, confirmationClicked = false) {
+ if (this.showFailedPipelineModal && !confirmationClicked) {
+ this.isPipelineFailedModalVisible = true;
+ return;
+ }
+
if (mergeImmediately) {
this.isMergingImmediately = true;
}
@@ -386,7 +396,7 @@ export default {
}
},
onMergeImmediatelyConfirmation() {
- this.handleMergeButtonClick(false, true);
+ this.handleMergeButtonClick(false, true, true);
},
initiateMergePolling() {
simplePoll(
@@ -522,6 +532,11 @@ export default {
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
</gl-dropdown>
+ <merge-train-failed-pipeline-confirmation-dialog
+ :visible="isPipelineFailedModalVisible"
+ @startMergeTrain="onStartMergeTrainConfirmation"
+ @cancel="isPipelineFailedModalVisible = false"
+ />
</gl-button-group>
<div
v-if="shouldShowMergeControls"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index c6ce29acb09..69e4df0ca11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -46,7 +46,7 @@ export default {
size="small"
icon="issue-new"
>
- {{ s__('mrWidget|Resolve all threads in new issue') }}
+ {{ s__('mrWidget|Create issue to resolve all threads') }}
</gl-button>
</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 a1eb77479bd..393c599c7e8 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
@@ -124,7 +124,7 @@ export default {
},
}) => {
createFlash({
- message: __('The merge request can now be merged.'),
+ message: __('Marked as ready. Merging is now allowed.'),
type: 'notice',
});
$('.merge-request .detail-page-description .title').text(title);
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
index 427ab0842ea..87a310efe78 100644
--- 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
@@ -104,7 +104,7 @@ export default {
:href="plan.job_path"
target="_blank"
data-testid="terraform-report-link"
- data-track-event="click_terraform_mr_plan_button"
+ 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"
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index d067e531fad..f5710f46b7e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -10,6 +10,8 @@ export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MT_MERGE_STRATEGY = 'merge_train';
+export const PIPELINE_FAILED_STATE = 'failed';
+
export const AUTO_MERGE_STRATEGIES = [MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY];
// SP - "Suggest Pipelines"
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 3a3a1329483..f5dbcec7dbe 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -6,9 +6,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate';
-import { registerExtension } from './components/extensions';
-import issueExtension from './extensions/issues';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -28,13 +27,13 @@ export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url;
- registerExtension(issueExtension);
-
const vm = new Vue({
el: '#js-vue-mr-widget',
provide: {
artifactsEndpoint: gl.mrWidgetData.artifacts_endpoint,
artifactsEndpointPlaceholder: gl.mrWidgetData.artifacts_endpoint_placeholder,
+ falsePositiveDocUrl: gl.mrWidgetData.false_positive_doc_url,
+ canViewFalsePositive: parseBoolean(gl.mrWidgetData.can_view_false_positive),
},
...MrWidgetOptions,
apolloProvider,
diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
index 23215982e6e..9d8e5d12d58 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js
@@ -38,5 +38,13 @@ export default {
pipelineId() {
return this.pipeline.id;
},
+ showFailedPipelineModal() {
+ return false;
+ },
+ },
+ methods: {
+ onStartMergeTrainConfirmation() {
+ return false;
+ },
},
};
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 a8a9df598f5..78aa3941bfe 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
@@ -12,9 +12,6 @@ import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
import { setFaviconOverlay } from '../lib/utils/favicon';
-import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
-import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
-import GroupedTestReportsApp from '../reports/grouped_test_report/grouped_test_reports_app.vue';
import Loading from './components/loading.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
import WidgetHeader from './components/mr_widget_header.vue';
@@ -42,7 +39,6 @@ import ShaMismatch from './components/states/sha_mismatch.vue';
import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
import WorkInProgressState from './components/states/work_in_progress.vue';
// import ExtensionsContainer from './components/extensions/container';
-import TerraformPlan from './components/terraform/mr_widget_terraform_container.vue';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
@@ -72,7 +68,9 @@ export default {
'mr-widget-nothing-to-merge': NothingToMergeState,
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
- 'mr-widget-ready-to-merge': ReadyToMergeState,
+ 'mr-widget-ready-to-merge': window.gon?.features?.restructuredMrWidget
+ ? () => import('./components/states/new_ready_to_merge.vue')
+ : ReadyToMergeState,
'sha-mismatch': ShaMismatch,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
@@ -82,12 +80,16 @@ export default {
'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus,
- GroupedCodequalityReportsApp,
- GroupedTestReportsApp,
- TerraformPlan,
- GroupedAccessibilityReportsApp,
+ GroupedCodequalityReportsApp: () =>
+ 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'),
},
apollo: {
state: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
new file mode 100644
index 00000000000..3b34be73c15
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/new_ready_to_merge.query.graphql
@@ -0,0 +1,9 @@
+query readyToMergeQuery($projectPath: ID!, $iid: String!) {
+ project(fullPath: $projectPath) {
+ mergeRequest(iid: $iid) {
+ userPermissions {
+ canMerge
+ }
+ }
+ }
+}
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 04800cf43f0..65d78fc283c 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,7 +1,9 @@
import { stateKey } from './state_maps';
export default function deviseState() {
- if (this.projectArchived) {
+ if (this.hasMergeChecksFailed) {
+ return stateKey.mergeChecksFailed;
+ } else if (this.projectArchived) {
return stateKey.archived;
} else if (this.branchMissing) {
return stateKey.missingBranch;
@@ -25,7 +27,7 @@ export default function deviseState() {
return stateKey.shaMismatch;
} else if (this.autoMergeEnabled && !this.mergeError) {
return stateKey.autoMergeEnabled;
- } else if (!this.canMerge) {
+ } else if (!this.canMerge && !window.gon?.features?.restructuredMrWidget) {
return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
return stateKey.readyToMerge;
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 8979fe621ac..29e0c867f6b 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
@@ -347,4 +347,13 @@ export default class MergeRequestStore {
this.approvals = data;
this.isApproved = data.approved || false;
}
+
+ get hasMergeChecksFailed() {
+ if (!window.gon?.features?.restructuredMrWidget) return false;
+
+ return (
+ this.hasMergeableDiscussionsState ||
+ (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed)
+ );
+ }
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 28507bba3e5..04454882666 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -18,6 +18,7 @@ const stateToComponentMap = {
autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'sha-mismatch',
rebase: 'mr-widget-rebase',
+ mergeChecksFailed: 'mergeChecksFailed',
};
const statesToShowHelpWidget = [
@@ -50,6 +51,7 @@ export const stateKey = {
readyToMerge: 'readyToMerge',
rebase: 'rebase',
merged: 'merged',
+ mergeChecksFailed: 'mergeChecksFailed',
};
export default {
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index 3705e36a579..f8f1613879f 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import NoteHeader from '~/notes/components/note_header.vue';
@@ -40,7 +39,7 @@ export default {
<div class="note-header">
<note-header :author="noteAuthor" :created-at="note.createdAt" :note-id="note.id">
- <span v-html="note.bodyHtml"></span>
+ <span v-html="note.bodyHtml /* eslint-disable-line vue/no-v-html */"></span>
</note-header>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index f4c73d12923..82a28d4cb5f 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
import { groupBy } from 'lodash';
import EmojiPicker from '~/emoji/components/picker.vue';
import { __, sprintf } from '~/locale';
@@ -18,6 +17,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -164,6 +164,7 @@ export default {
this.isMenuOpen = menuOpen;
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
@@ -180,7 +181,11 @@ export default {
@click="handleAward(awardList.name)"
>
<template #emoji>
- <span class="award-emoji-block" data-testid="award-html" v-html="awardList.html"></span>
+ <span
+ v-safe-html:[$options.safeHtmlConfig]="awardList.html"
+ class="award-emoji-block"
+ data-testid="award-html"
+ ></span>
</template>
<span class="js-counter">{{ awardList.list.length }}</span>
</gl-button>
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 0589b47edbd..84770dbac6f 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { HIGHLIGHT_CLASS_NAME } from './constants';
@@ -75,7 +74,9 @@ export default {
</a>
</div>
<div class="blob-content">
- <pre class="code highlight"><code :data-blob-hash="blobHash" v-html="content"></code></pre>
+ <pre
+ class="code highlight"
+ ><code :data-blob-hash="blobHash" v-html="content /* eslint-disable-line vue/no-v-html */"></code></pre>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/code_block.vue b/app/assets/javascripts/vue_shared/components/code_block.vue
index 1928bf6dac5..9856f35c7f6 100644
--- a/app/assets/javascripts/vue_shared/components/code_block.vue
+++ b/app/assets/javascripts/vue_shared/components/code_block.vue
@@ -24,8 +24,13 @@ export default {
return isScrollable ? scrollableStyles : null;
},
},
+ userColorScheme: window.gon.user_color_scheme,
};
</script>
<template>
- <pre class="code-block rounded" :style="styleObject"><code class="d-block">{{ code }}</code></pre>
+ <pre
+ class="code-block rounded code"
+ :class="$options.userColorScheme"
+ :style="styleObject"
+ ><code class="d-block">{{ code }}</code></pre>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
index 0ff33e462b4..3c21b14894b 100644
--- a/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/color_picker/color_picker.vue
@@ -110,7 +110,7 @@ export default {
<div :class="previewColorClasses" :style="previewColor" data-testid="color-preview">
<gl-form-input
type="color"
- class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-cursor-pointer gl-opacity-0"
+ class="gl-absolute gl-top-0 gl-left-0 gl-h-full! gl-p-0! gl-m-0! gl-opacity-0"
tabindex="-1"
:value="value"
@input="handleColorChange"
diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue
index d1eee62683b..5f50a699034 100644
--- a/app/assets/javascripts/vue_shared/components/commit.vue
+++ b/app/assets/javascripts/vue_shared/components/commit.vue
@@ -136,6 +136,9 @@ export default {
refUrl() {
return this.commitRef.ref_url || this.commitRef.path;
},
+ tooltipTitle() {
+ return this.mergeRequestRef ? this.mergeRequestRef.title : this.commitRef.name;
+ },
},
};
</script>
@@ -148,23 +151,14 @@ export default {
<gl-icon v-else name="branch" />
</div>
- <gl-link
- v-if="mergeRequestRef"
- v-gl-tooltip
- :href="mergeRequestRef.path"
- :title="mergeRequestRef.title"
- class="ref-name"
- >{{ mergeRequestRef.iid }}</gl-link
- >
- <gl-link
- v-else
- v-gl-tooltip
- :href="refUrl"
- :title="commitRef.name"
- class="ref-name"
- data-testid="ref-name"
- >{{ commitRef.name }}</gl-link
- >
+ <tooltip-on-truncate :title="tooltipTitle" truncate-target="child" placement="top">
+ <gl-link v-if="mergeRequestRef" :href="mergeRequestRef.path" class="ref-name">
+ {{ mergeRequestRef.iid }}
+ </gl-link>
+ <gl-link v-else :href="refUrl" class="ref-name" data-testid="ref-name">
+ {{ commitRef.name }}
+ </gl-link>
+ </tooltip-on-truncate>
</template>
<gl-icon name="commit" class="commit-icon js-commit-icon" />
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 3790a509f26..7b88b36aa0f 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
@@ -110,6 +109,10 @@ export default {
<template>
<div ref="markdownPreview" class="md-previewer" data-testid="md-previewer">
<gl-skeleton-loading v-if="isLoading" />
- <div v-else class="md gl-ml-auto gl-mr-auto" v-html="previewContent"></div>
+ <div
+ v-else
+ class="md gl-ml-auto gl-mr-auto"
+ v-html="previewContent /* eslint-disable-line vue/no-v-html */"
+ ></div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
new file mode 100644
index 00000000000..56e6399a1b7
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/diff_stats_dropdown.vue
@@ -0,0 +1,159 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlSprintf,
+} from '@gitlab/ui';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { __, n__, s__, sprintf } from '~/locale';
+
+export const i18n = {
+ messageAdditionsDeletions: s__('Diffs|with %{additions} and %{deletions}'),
+ noFilesFound: __('No files found.'),
+ noFileNameAvailable: s__('Diffs|No file name available'),
+ searchFiles: __('Search files'),
+};
+
+export default {
+ i18n,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ GlSprintf,
+ },
+ props: {
+ changed: {
+ type: Number,
+ required: true,
+ },
+ added: {
+ type: Number,
+ required: true,
+ },
+ deleted: {
+ type: Number,
+ required: true,
+ },
+ files: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ search: '',
+ };
+ },
+ computed: {
+ filteredFiles() {
+ return this.search.length > 0
+ ? fuzzaldrinPlus.filter(this.files, this.search, { key: 'name' })
+ : this.files;
+ },
+ messageChanged() {
+ return sprintf(
+ n__(
+ 'Diffs|Showing %{dropdownStart}%{count} changed file%{dropdownEnd}',
+ 'Diffs|Showing %{dropdownStart}%{count} changed files%{dropdownEnd}',
+ this.changed,
+ ),
+ { count: this.changed },
+ );
+ },
+
+ additionsText() {
+ return n__('Diffs|%d addition', 'Diffs|%d additions', this.added);
+ },
+ deletionsText() {
+ return n__('Diffs|%d deletion', 'Diffs|%d deletions', this.deleted);
+ },
+ },
+ methods: {
+ jumpToFile(fileHash) {
+ window.location.hash = fileHash;
+ },
+ focusInput() {
+ this.$refs.search.focusInput();
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-sprintf :message="messageChanged">
+ <template #dropdown="{ content: dropdownText }">
+ <gl-dropdown
+ category="tertiary"
+ variant="confirm"
+ :text="dropdownText"
+ data-testid="diff-stats-dropdown"
+ class="gl-vertical-align-baseline"
+ toggle-class="gl-px-0! gl-font-weight-bold!"
+ menu-class="gl-w-auto!"
+ no-flip
+ @shown="focusInput"
+ >
+ <template #header>
+ <gl-search-box-by-type
+ ref="search"
+ v-model.trim="search"
+ :placeholder="$options.i18n.searchFiles"
+ />
+ </template>
+ <gl-dropdown-item
+ v-for="file in filteredFiles"
+ :key="file.href"
+ :icon-name="file.icon"
+ :icon-color="file.iconColor"
+ @click="jumpToFile(file.href)"
+ >
+ <div class="gl-display-flex">
+ <span v-if="file.name" class="gl-font-weight-bold gl-mr-3 gl-text-truncate">{{
+ file.name
+ }}</span>
+ <span v-else class="gl-mr-3 gl-font-weight-bold gl-font-style-italic gl-gray-400">{{
+ $options.i18n.noFileNameAvailable
+ }}</span>
+ <span class="gl-ml-auto gl-white-space-nowrap">
+ <span class="gl-text-green-600">+{{ file.added }}</span>
+ <span class="gl-text-red-500">-{{ file.removed }}</span>
+ </span>
+ </div>
+ <div class="gl-text-gray-700 gl-overflow-hidden gl-text-overflow-ellipsis">
+ {{ file.path }}
+ </div>
+ </gl-dropdown-item>
+ <gl-dropdown-text v-if="!filteredFiles.length">
+ {{ $options.i18n.noFilesFound }}
+ </gl-dropdown-text>
+ </gl-dropdown>
+ </template>
+ </gl-sprintf>
+ <span
+ class="diff-stats-additions-deletions-expanded"
+ data-testid="diff-stats-additions-deletions-expanded"
+ >
+ <gl-sprintf :message="$options.i18n.messageAdditionsDeletions">
+ <template #additions>
+ <span class="gl-text-green-600 gl-font-weight-bold">{{ additionsText }}</span>
+ </template>
+ <template #deletions>
+ <span class="gl-text-red-500 gl-font-weight-bold">{{ deletionsText }}</span>
+ </template>
+ </gl-sprintf>
+ </span>
+
+ <div
+ class="diff-stats-additions-deletions-collapsed gl-float-right gl-display-none"
+ data-testid="diff-stats-additions-deletions-collapsed"
+ >
+ <span class="gl-text-green-600 gl-font-weight-bold">+{{ added }}</span>
+ <span class="gl-text-red-500 gl-font-weight-bold">-{{ deleted }}</span>
+ </div>
+ </div>
+</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 2e9634819a0..1df65d0a666 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
@@ -20,19 +20,26 @@ export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_
export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }];
export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY];
-export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __(FILTER_NONE) };
-export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __(FILTER_ANY) };
+export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
+export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
- { value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
+ { value: FILTER_CURRENT, text: __('Current') },
]);
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
- { value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) },
- { value: FILTER_STARTED, text: __(FILTER_STARTED) },
+ { value: FILTER_UPCOMING, text: __('Upcoming'), title: __('Upcoming') },
+ { value: FILTER_STARTED, text: __('Started'), title: __('Started') },
]);
+export const DEFAULT_MILESTONES_GRAPHQL = [
+ { value: 'any', text: __('Any'), title: __('Any') },
+ { value: 'none', text: __('None'), title: __('None') },
+ { value: '#upcoming', text: __('Upcoming'), title: __('Upcoming') },
+ { value: '#started', text: __('Started'), title: __('Started') },
+];
+
export const SortDirection = {
descending: 'descending',
ascending: 'ascending',
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index 6573f366b52..5cc96471aef 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -177,13 +177,10 @@ function filteredSearchTermValue(value) {
* @param {Object} options
* @param {String} [options.filteredSearchTermKey] if set, a FILTERED_SEARCH_TERM filter is created to this parameter. `'search'` is suggested
* @param {String[]} [options.filterNamesAllowList] if set, only this list of filters names is mapped
- * @param {Boolean} [options.legacySpacesDecode] if set, plus symbols (+) are not encoded as spaces. `false` is suggested
* @return {Object} filter object with filter names and their values
*/
-export function urlQueryToFilter(query = '', options = {}) {
- const { filteredSearchTermKey, filterNamesAllowList, legacySpacesDecode = true } = options;
-
- const filters = queryToObject(query, { gatherArrays: true, legacySpacesDecode });
+export function urlQueryToFilter(query = '', { filteredSearchTermKey, filterNamesAllowList } = {}) {
+ const filters = queryToObject(query, { gatherArrays: true });
return Object.keys(filters).reduce((memo, key) => {
const value = filters[key];
if (!value) {
@@ -222,7 +219,7 @@ export function urlQueryToFilter(query = '', options = {}) {
*/
export function getRecentlyUsedSuggestions(recentSuggestionsStorageKey) {
let recentlyUsedSuggestions = [];
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
recentlyUsedSuggestions = JSON.parse(localStorage.getItem(recentSuggestionsStorageKey)) || [];
}
return recentlyUsedSuggestions;
@@ -240,7 +237,7 @@ export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenVa
recentlyUsedSuggestions.splice(0, 0, { ...tokenValue });
- if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ if (AccessorUtilities.canUseLocalStorage()) {
localStorage.setItem(
recentSuggestionsStorageKey,
JSON.stringify(uniqWith(recentlyUsedSuggestions, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
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 4b9ad6d8f91..523438f459c 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
@@ -39,8 +39,16 @@ export default {
},
methods: {
getActiveMilestone(milestones, data) {
- return milestones.find(
- (milestone) => milestone.title.toLowerCase() === stripQuotes(data).toLowerCase(),
+ /* We need to check default milestones against the value not the
+ * title because there is a discrepancy between the value graphql
+ * accepts and the title.
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/337687#note_648058797
+ */
+
+ return (
+ milestones.find(
+ (milestone) => milestone.title.toLowerCase() === stripQuotes(data).toLowerCase(),
+ ) || this.defaultMilestones.find(({ value }) => value === data)
);
},
fetchMilestones(searchTerm) {
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index f169921d8a6..41613bb3307 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,6 +1,5 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlTooltipDirective, GlLink, GlButton, GlTooltip } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlButton, GlTooltip, GlSafeHtmlDirective } from '@gitlab/ui';
import { glEmojiTag } from '../../emoji';
import { __, sprintf } from '../../locale';
import CiIconBadge from './ci_badge_link.vue';
@@ -25,6 +24,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
+ SafeHtml: GlSafeHtmlDirective,
},
EMOJI_REF: 'EMOJI_REF',
props: {
@@ -37,8 +37,9 @@ export default {
required: true,
},
itemId: {
- type: Number,
- required: true,
+ type: String,
+ required: false,
+ default: '',
},
time: {
type: String,
@@ -86,6 +87,13 @@ export default {
message() {
return this.user?.status?.message;
},
+ item() {
+ if (this.itemId) {
+ return `${this.itemName} #${this.itemId}`;
+ }
+
+ return this.itemName;
+ },
},
methods: {
@@ -93,6 +101,7 @@ export default {
this.$emit('clickedSidebarButton');
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
@@ -105,7 +114,7 @@ export default {
<section class="header-main-content gl-mr-3">
<ci-icon-badge :status="status" />
- <strong data-testid="ci-header-item-text"> {{ itemName }} #{{ itemId }} </strong>
+ <strong data-testid="ci-header-item-text">{{ item }}</strong>
<template v-if="shouldRenderTriggeredLabel">{{ __('triggered') }}</template>
<template v-else>{{ __('created') }}</template>
@@ -130,8 +139,8 @@ export default {
<span
v-if="statusTooltipHTML"
:ref="$options.EMOJI_REF"
+ v-safe-html:[$options.safeHtmlConfig]="statusTooltipHTML"
:data-testid="message"
- v-html="statusTooltipHTML"
></span>
</template>
</section>
diff --git a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js b/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
index 18bfcc268dc..28aa93d6680 100644
--- a/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
+++ b/app/assets/javascripts/vue_shared/components/issuable/init_issuable_header_warning.js
@@ -1,10 +1,20 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import IssuableHeaderWarnings from './issuable_header_warnings.vue';
export default function issuableHeaderWarnings(store) {
+ const el = document.getElementById('js-issuable-header-warnings');
+
+ if (!el) {
+ return false;
+ }
+
+ const { hidden } = el.dataset;
+
return new Vue({
- el: document.getElementById('js-issuable-header-warnings'),
+ el,
store,
+ provide: { hidden: parseBoolean(hidden) },
render(createElement) {
return createElement(IssuableHeaderWarnings);
},
diff --git a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
index 56adbe8c606..82223ab9ef4 100644
--- a/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
+++ b/app/assets/javascripts/vue_shared/components/issuable/issuable_header_warnings.vue
@@ -1,11 +1,16 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import { __ } from '~/locale';
export default {
components: {
GlIcon,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['hidden'],
computed: {
...mapGetters(['getNoteableData']),
isLocked() {
@@ -26,6 +31,12 @@ export default {
visible: this.isConfidential,
dataTestId: 'confidential',
},
+ {
+ iconName: 'spam',
+ visible: this.hidden,
+ dataTestId: 'hidden',
+ tooltip: __('This issue is hidden because its author has been banned'),
+ },
];
},
},
@@ -35,8 +46,15 @@ export default {
<template>
<div class="gl-display-inline-block">
<template v-for="meta in warningIconsMeta">
- <div v-if="meta.visible" :key="meta.iconName" class="issuable-warning-icon inline">
- <gl-icon :name="meta.iconName" :data-testid="meta.dataTestId" class="icon" />
+ <div
+ v-if="meta.visible"
+ :key="meta.iconName"
+ v-gl-tooltip
+ :data-testid="meta.dataTestId"
+ :title="meta.tooltip || null"
+ class="issuable-warning-icon inline"
+ >
+ <gl-icon :name="meta.iconName" class="icon" />
</div>
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
index ccdb47e3144..095d1854c8b 100644
--- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import '~/commons/bootstrap';
import { GlIcon, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
@@ -72,7 +71,7 @@ export default {
class="item-contents gl-display-flex gl-align-items-center gl-flex-wrap gl-flex-grow-1 flex-xl-nowrap gl-min-h-7"
>
<!-- Title area: Status icon (XL) and title -->
- <div class="item-title d-flex align-items-xl-center mb-xl-0">
+ <div class="item-title d-flex align-items-xl-center mb-xl-0 gl-min-w-0">
<div ref="iconElementXL">
<gl-icon
v-if="hasState"
@@ -85,7 +84,7 @@ export default {
/>
</div>
<gl-tooltip :target="() => $refs.iconElementXL">
- <span v-html="stateTitle"></span>
+ <span v-html="stateTitle /* eslint-disable-line vue/no-v-html */"></span>
</gl-tooltip>
<gl-icon
v-if="confidential"
@@ -111,7 +110,7 @@ export default {
class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2"
>
<gl-tooltip :target="() => this.$refs.iconElement">
- <span v-html="stateTitle"></span>
+ <span v-html="stateTitle /* eslint-disable-line vue/no-v-html */"></span>
</gl-tooltip>
<span v-gl-tooltip :title="itemPath" class="path-id-text d-inline-block">{{
itemPath
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 9ea48050079..77730ada9bb 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
@@ -15,6 +14,10 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
+function cleanUpLine(content) {
+ return unescape(stripHtml(content).replace(/\\n/g, '%br').replace(/\n/g, ''));
+}
+
export default {
components: {
GfmAutocomplete,
@@ -129,7 +132,7 @@ export default {
return text;
}
- return unescape(stripHtml(richText).replace(/\n/g, ''));
+ return cleanUpLine(richText);
})
.join('\\n');
}
@@ -141,7 +144,7 @@ export default {
return text;
}
- return unescape(stripHtml(richText).replace(/\n/g, ''));
+ return cleanUpLine(richText);
}
return '';
@@ -272,6 +275,7 @@ export default {
:can-suggest="canSuggest"
:show-suggest-popover="showSuggestPopover"
:suggestion-start-index="suggestionsStartIndex"
+ data-testid="markdownHeader"
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
@@ -319,14 +323,20 @@ export default {
v-show="previewMarkdown"
ref="markdown-preview"
class="js-vue-md-preview md md-preview-holder"
- v-html="markdownPreview"
+ v-html="markdownPreview /* eslint-disable-line vue/no-v-html */"
></div>
</template>
<template v-if="previewMarkdown && !markdownPreviewLoading">
- <div v-if="referencedCommands" class="referenced-commands" v-html="referencedCommands"></div>
+ <div
+ v-if="referencedCommands"
+ class="referenced-commands"
+ v-html="referencedCommands /* eslint-disable-line vue/no-v-html */"
+ ></div>
<div v-if="shouldShowReferencedUsers" class="referenced-users">
<gl-icon name="warning-solid" />
- <span v-html="addMultipleToDiscussionWarning"></span>
+ <span
+ v-html="addMultipleToDiscussionWarning /* eslint-disable-line vue/no-v-html */"
+ ></span>
</div>
</template>
</div>
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 065d9b1b5dd..5fdef0b1a23 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
@@ -39,7 +39,8 @@ export default {
},
defaultCommitMessage: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
inapplicableReason: {
type: String,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 7112295fa57..912aa8ce294 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -75,7 +75,7 @@ export default {
variant="link"
:track-experiment="$options.inviteMembersInComment"
:trigger-source="$options.inviteMembersInComment"
- data-track-event="comment_invite_click"
+ data-track-action="comment_invite_click"
/>
<span class="uploading-progress-container hide">
<gl-icon name="media" />
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index ad6f6e0e2e3..0b302f22062 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlLink, GlIcon } from '@gitlab/ui';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
@@ -92,7 +91,9 @@ export default {
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
- <span v-html="confidentialAndLockedDiscussionText"></span>
+ <span
+ v-html="confidentialAndLockedDiscussionText /* eslint-disable-line vue/no-v-html */"
+ ></span>
{{
__("People without permission will never get a notification and won't be able to comment.")
}}
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 c3d861d74bc..755e6f1f224 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -1,6 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
-
/**
* Common component to render a system note, icon and user information.
*
@@ -97,6 +95,9 @@ export default {
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
};
</script>
@@ -106,7 +107,7 @@ export default {
:class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
class="note system-note note-wrapper"
>
- <div class="timeline-icon" v-html="iconHtml"></div>
+ <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">
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 8a67754993d..6867b5a75e3 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
@@ -1,5 +1,12 @@
<script>
-import { GlAlert, GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlBadge,
+ GlPagination,
+ GlTab,
+ GlTabs,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
import Api from '~/api';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -20,6 +27,9 @@ export default {
GlTab,
FilteredSearchBar,
},
+ directives: {
+ SafeHtml,
+ },
inject: {
projectPath: {
default: '',
@@ -265,8 +275,7 @@ export default {
<template>
<div class="incident-management-list">
<gl-alert v-if="showErrorMsg" variant="danger" @dismiss="$emit('error-alert-dismissed')">
- <!-- eslint-disable-next-line vue/no-v-html -->
- <p v-html="serverErrorMessage || i18n.errorMsg"></p>
+ <p v-safe-html="serverErrorMessage || i18n.errorMsg"></p>
</gl-alert>
<div
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 69f43c9e464..36d3696ec36 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable vue/no-v-html */
import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
import highlight from '~/lib/utils/highlight';
@@ -61,7 +60,7 @@ export default {
<div
:title="project.name"
class="js-project-name text-truncate"
- v-html="highlightedProjectName"
+ v-html="highlightedProjectName /* eslint-disable-line vue/no-v-html */"
></div>
</div>
</gl-button>
diff --git a/app/assets/javascripts/vue_shared/components/registry/title_area.vue b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
index c63d91b78d3..4b21ec0330a 100644
--- a/app/assets/javascripts/vue_shared/components/registry/title_area.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/title_area.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatar, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { isEqual } from 'lodash';
export default {
name: 'TitleArea',
@@ -36,13 +37,21 @@ export default {
metadataSlots: [],
};
},
- async mounted() {
- const METADATA_PREFIX = 'metadata-';
- this.metadataSlots = Object.keys(this.$slots).filter((k) => k.startsWith(METADATA_PREFIX));
+ mounted() {
+ this.recalculateMetadataSlots();
+ },
+ updated() {
+ this.recalculateMetadataSlots();
+ },
+ methods: {
+ recalculateMetadataSlots() {
+ const METADATA_PREFIX = 'metadata-';
+ const metadataSlots = Object.keys(this.$slots).filter((k) => k.startsWith(METADATA_PREFIX));
- // we need to wait for next tick to ensure that dynamic names slots are picked up
- await this.$nextTick();
- this.metadataSlots = Object.keys(this.$slots).filter((k) => k.startsWith(METADATA_PREFIX));
+ if (!isEqual(metadataSlots, this.metadataSlots)) {
+ this.metadataSlots = metadataSlots;
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
index f21dea468cb..57cc25caa25 100644
--- a/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue
@@ -1,5 +1,6 @@
<script>
import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import awsCloudFormationImageUrl from 'images/aws-cloud-formation.png';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { getBaseURL, objectToQuery } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
@@ -22,6 +23,11 @@ export default {
type: String,
required: true,
},
+ imgSrc: {
+ type: String,
+ required: false,
+ default: awsCloudFormationImageUrl,
+ },
},
methods: {
easyButtonUrl(easyButton) {
@@ -76,7 +82,7 @@ export default {
<img
:title="easyButton.stackName"
:alt="easyButton.stackName"
- src="/assets/aws-cloud-formation.png"
+ :src="imgSrc"
width="46"
height="46"
class="gl-mt-2 gl-mr-5 gl-mb-6"
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.stories.js b/app/assets/javascripts/vue_shared/components/settings/settings_block.stories.js
new file mode 100644
index 00000000000..5242743ad30
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.stories.js
@@ -0,0 +1,26 @@
+import SettingsBlock from './settings_block.vue';
+
+export default {
+ component: SettingsBlock,
+ title: 'vue_shared/components/settings/settings_block',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { SettingsBlock },
+ props: Object.keys(argTypes),
+ template: `
+ <settings-block v-bind="$props">
+ <template #title>Settings section title</template>
+ <template #description>Settings section description</template>
+ <template #default>
+ <p>Content</p>
+ <p>More content</p>
+ <p>Content</p>
+ <p>More content...</p>
+ <p>Content</p>
+ </template>
+ </settings-block>
+ `,
+});
+
+export const Default = Template.bind({});
diff --git a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
index 92ae4575c52..e75fedbb1d7 100644
--- a/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
+++ b/app/assets/javascripts/vue_shared/components/settings/settings_block.vue
@@ -1,5 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
import { __ } from '~/locale';
export default {
@@ -15,35 +17,99 @@ export default {
default: false,
required: false,
},
+ collapsible: {
+ type: Boolean,
+ default: true,
+ required: false,
+ },
},
data() {
return {
- sectionExpanded: false,
+ // Non-collapsible sections should always be expanded.
+ // For collapsible sections, fall back to defaultExpanded.
+ sectionExpanded: !this.collapsible || this.defaultExpanded,
};
},
computed: {
- expanded() {
- return this.defaultExpanded || this.sectionExpanded;
- },
toggleText() {
- return this.expanded ? __('Collapse') : __('Expand');
+ const { collapseText, expandText } = this.$options.i18n;
+ return this.sectionExpanded ? collapseText : expandText;
+ },
+ settingsContentId() {
+ return uniqueId('settings_content_');
},
+ settingsLabelId() {
+ return uniqueId('settings_label_');
+ },
+ toggleButtonAriaLabel() {
+ const { collapseAriaLabel, expandAriaLabel } = this.$options.i18n;
+ return this.sectionExpanded ? collapseAriaLabel : expandAriaLabel;
+ },
+ ariaExpanded() {
+ return String(this.sectionExpanded);
+ },
+ },
+ methods: {
+ toggleSectionExpanded() {
+ this.sectionExpanded = !this.sectionExpanded;
+
+ if (this.sectionExpanded) {
+ this.$refs.settingsContent.focus();
+ }
+ },
+ },
+ i18n: {
+ collapseText: __('Collapse'),
+ expandText: __('Expand'),
+ collapseAriaLabel: __('Collapse settings section'),
+ expandAriaLabel: __('Expand settings section'),
},
};
</script>
<template>
- <section class="settings" :class="{ 'no-animate': !slideAnimated, expanded }">
+ <section class="settings" :class="{ 'no-animate': !slideAnimated, expanded: sectionExpanded }">
<div class="settings-header">
- <h4><slot name="title"></slot></h4>
- <gl-button @click="sectionExpanded = !sectionExpanded">
+ <h4>
+ <span
+ v-if="collapsible"
+ :id="settingsLabelId"
+ role="button"
+ tabindex="0"
+ class="gl-cursor-pointer"
+ :aria-controls="settingsContentId"
+ :aria-expanded="ariaExpanded"
+ data-testid="section-title-button"
+ @click="toggleSectionExpanded"
+ @keydown.enter.space="toggleSectionExpanded"
+ >
+ <slot name="title"></slot>
+ </span>
+ <template v-else>
+ <slot name="title"></slot>
+ </template>
+ </h4>
+ <gl-button
+ v-if="collapsible"
+ :aria-controls="settingsContentId"
+ :aria-expanded="ariaExpanded"
+ :aria-label="toggleButtonAriaLabel"
+ @click="toggleSectionExpanded"
+ >
{{ toggleText }}
</gl-button>
<p>
<slot name="description"></slot>
</p>
</div>
- <div class="settings-content">
+ <div
+ :id="settingsContentId"
+ ref="settingsContent"
+ :aria-labelledby="settingsLabelId"
+ tabindex="-1"
+ role="region"
+ class="settings-content"
+ >
<slot></slot>
</div>
</section>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
index 46ccb9470e5..35ac9ef8565 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
@@ -1,5 +1,6 @@
<script>
import { GlLabel } from '@gitlab/ui';
+import { sortBy } from 'lodash';
import { mapState } from 'vuex';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -23,6 +24,9 @@ export default {
'labelsFilterBasePath',
'labelsFilterParam',
]),
+ sortedSelectedLabels() {
+ return sortBy(this.selectedLabels, (label) => (isScopedLabel(label) ? 0 : 1));
+ },
},
methods: {
labelFilterUrl(label) {
@@ -47,7 +51,7 @@ export default {
<span v-if="!selectedLabels.length" class="text-secondary">
<slot></slot>
</span>
- <template v-for="label in selectedLabels" v-else>
+ <template v-for="label in sortedSelectedLabels" v-else>
<gl-label
:key="label.id"
data-qa-selector="selected_label_content"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
index e8fdf4bb0c2..dd40add6376 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
@@ -56,7 +56,7 @@ export default {
const labelLink = h(
GlLink,
{
- class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal',
+ class: 'gl-display-flex gl-align-items-center label-item gl-text-body',
on: {
click: () => {
listeners.clickLabel(label);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
deleted file mode 100644
index 60111210f5d..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_button.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { mapActions, mapGetters } from 'vuex';
-
-export default {
- components: {
- GlButton,
- GlIcon,
- },
- computed: {
- ...mapGetters([
- 'dropdownButtonText',
- 'isDropdownVariantStandalone',
- 'isDropdownVariantEmbedded',
- ]),
- },
- methods: {
- ...mapActions(['toggleDropdownContents']),
- handleButtonClick(e) {
- if (this.isDropdownVariantStandalone || this.isDropdownVariantEmbedded) {
- this.toggleDropdownContents();
- }
-
- if (this.isDropdownVariantStandalone) {
- e.stopPropagation();
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-button
- class="labels-select-dropdown-button js-dropdown-button w-100 text-left"
- @click="handleButtonClick"
- >
- <span class="dropdown-toggle-text gl-pointer-events-none flex-fill">
- {{ dropdownButtonText }}
- </span>
- <gl-icon name="chevron-down" class="gl-pointer-events-none float-right" />
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 6694e349b6e..0fcc67c0ffa 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -1,22 +1,21 @@
<script>
-import { GlButton } from '@gitlab/ui';
-import { mapActions, mapGetters, mapState } from 'vuex';
+import { GlButton, GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import DropdownContentsCreateView from './dropdown_contents_create_view.vue';
import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
+import { isDropdownVariantSidebar, isDropdownVariantEmbedded } from './utils';
export default {
components: {
DropdownContentsLabelsView,
DropdownContentsCreateView,
GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlLink,
},
+ inject: ['allowLabelCreate', 'labelsManagePath'],
props: {
- renderOnTop: {
- type: Boolean,
- required: false,
- default: false,
- },
labelsCreateTitle: {
type: String,
required: true,
@@ -33,6 +32,10 @@ export default {
type: String,
required: true,
},
+ dropdownButtonText: {
+ type: String,
+ required: true,
+ },
footerCreateLabelTitle: {
type: String,
required: true,
@@ -41,70 +44,105 @@ export default {
type: String,
required: true,
},
+ variant: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showDropdownContentsCreateView: false,
+ };
},
computed: {
- ...mapState(['showDropdownContentsCreateView']),
- ...mapGetters(['isDropdownVariantSidebar', 'isDropdownVariantEmbedded']),
dropdownContentsView() {
if (this.showDropdownContentsCreateView) {
return 'dropdown-contents-create-view';
}
return 'dropdown-contents-labels-view';
},
- directionStyle() {
- const bottom = this.isDropdownVariantSidebar ? '3rem' : '2rem';
- return this.renderOnTop ? { bottom } : {};
- },
dropdownTitle() {
return this.showDropdownContentsCreateView ? this.labelsCreateTitle : this.labelsListTitle;
},
+ showDropdownFooter() {
+ return (
+ !this.showDropdownContentsCreateView &&
+ (this.isDropdownVariantSidebar(this.variant) ||
+ this.isDropdownVariantEmbedded(this.variant))
+ );
+ },
},
methods: {
- ...mapActions(['toggleDropdownContentsCreateView', 'toggleDropdownContents']),
+ showDropdown() {
+ this.$refs.dropdown.show();
+ },
+ toggleDropdownContentsCreateView() {
+ this.showDropdownContentsCreateView = !this.showDropdownContentsCreateView;
+ },
+ toggleDropdownContent() {
+ this.toggleDropdownContentsCreateView();
+ // Required to recalculate dropdown position as its size changes
+ this.$refs.dropdown.$refs.dropdown.$_popper.scheduleUpdate();
+ },
+ isDropdownVariantSidebar,
+ isDropdownVariantEmbedded,
},
};
</script>
<template>
- <div
- class="labels-select-dropdown-contents gl-w-full gl-my-2 gl-py-3 gl-rounded-base gl-absolute"
+ <gl-dropdown
+ ref="dropdown"
+ :text="dropdownButtonText"
+ class="gl-w-full gl-mt-2"
data-qa-selector="labels_dropdown_content"
- :style="directionStyle"
>
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
- data-testid="dropdown-title"
- >
- <gl-button
- v-if="showDropdownContentsCreateView"
- :aria-label="__('Go back')"
- variant="link"
- size="small"
- class="js-btn-back dropdown-header-button p-0"
- icon="arrow-left"
- @click="toggleDropdownContentsCreateView"
- />
- <span class="flex-grow-1">{{ dropdownTitle }}</span>
- <gl-button
- :aria-label="__('Close')"
- variant="link"
- size="small"
- class="dropdown-header-button gl-p-0!"
- icon="close"
- @click="toggleDropdownContents"
- />
- </div>
+ <template #header>
+ <div
+ v-if="isDropdownVariantSidebar(variant) || isDropdownVariantEmbedded(variant)"
+ class="dropdown-title gl-display-flex gl-align-items-center gl-pt-0 gl-pb-3!"
+ >
+ <gl-button
+ v-if="showDropdownContentsCreateView"
+ :aria-label="__('Go back')"
+ variant="link"
+ size="small"
+ class="js-btn-back dropdown-header-button gl-p-0"
+ icon="arrow-left"
+ data-testid="go-back-button"
+ @click.stop="toggleDropdownContent"
+ />
+ <span class="gl-flex-grow-1">{{ dropdownTitle }}</span>
+ <gl-button
+ :aria-label="__('Close')"
+ variant="link"
+ size="small"
+ class="dropdown-header-button gl-p-0!"
+ icon="close"
+ @click="$emit('closeDropdown')"
+ />
+ </div>
+ </template>
<component
:is="dropdownContentsView"
:selected-labels="selectedLabels"
:allow-multiselect="allowMultiselect"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
@hideCreateView="toggleDropdownContentsCreateView"
- @closeDropdown="$emit('closeDropdown', $event)"
- @toggleDropdownContentsCreateView="toggleDropdownContentsCreateView"
+ @setLabels="$emit('setLabels', $event)"
/>
- </div>
+ <template #footer>
+ <div v-if="showDropdownFooter" data-testid="dropdown-footer">
+ <gl-dropdown-item
+ v-if="allowLabelCreate"
+ data-testid="create-label-button"
+ @click.native.capture.stop="toggleDropdownContent"
+ >
+ {{ footerCreateLabelTitle }}
+ </gl-dropdown-item>
+ <gl-dropdown-item :href="labelsManagePath" @click.native.capture.stop>
+ {{ footerManageLabelTitle }}
+ </gl-dropdown-item>
+ </div>
+ </template>
+ </gl-dropdown>
</template>
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 4651e7a1576..2e31b386fdd 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
@@ -1,8 +1,10 @@
<script>
import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import produce from 'immer';
import createFlash from '~/flash';
import { __ } from '~/locale';
import createLabelMutation from './graphql/create_label.mutation.graphql';
+import projectLabelsQuery from './graphql/project_labels.query.graphql';
const errorMessage = __('Error creating label.');
@@ -47,6 +49,25 @@ export default {
handleColorClick(color) {
this.selectedColor = this.getColorCode(color);
},
+ updateLabelsInCache(store, label) {
+ const sourceData = store.readQuery({
+ query: projectLabelsQuery,
+ variables: { fullPath: this.projectPath, searchTerm: '' },
+ });
+
+ const collator = new Intl.Collator('en');
+ const data = produce(sourceData, (draftData) => {
+ const { nodes } = draftData.workspace.labels;
+ nodes.push(label);
+ nodes.sort((a, b) => collator.compare(a.title, b.title));
+ });
+
+ store.writeQuery({
+ query: projectLabelsQuery,
+ variables: { fullPath: this.projectPath, searchTerm: '' },
+ data,
+ });
+ },
async createLabel() {
this.labelCreateInProgress = true;
try {
@@ -59,6 +80,14 @@ export default {
color: this.selectedColor,
projectPath: this.projectPath,
},
+ update: (
+ store,
+ {
+ data: {
+ labelCreate: { label },
+ },
+ },
+ ) => this.updateLabelsInCache(store, label),
});
if (labelCreate.errors.length) {
createFlash({ message: errorMessage });
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 ffa37424c2c..857367a0721 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,24 +1,23 @@
<script>
-import { GlLoadingIcon, GlSearchBoxByType, GlLink } from '@gitlab/ui';
+import { GlDropdownForm, GlDropdownItem, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes';
import { __ } from '~/locale';
-import { DropdownVariant } from './constants';
import projectLabelsQuery from './graphql/project_labels.query.graphql';
import LabelItem from './label_item.vue';
export default {
components: {
+ GlDropdownForm,
+ GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
- GlLink,
LabelItem,
},
- inject: ['projectPath', 'allowLabelCreate', 'labelsManagePath', 'variant'],
+ inject: ['projectPath'],
props: {
selectedLabels: {
type: Array,
@@ -28,24 +27,11 @@ export default {
type: Boolean,
required: true,
},
- labelsListTitle: {
- type: String,
- required: true,
- },
- footerCreateLabelTitle: {
- type: String,
- required: true,
- },
- footerManageLabelTitle: {
- type: String,
- required: true,
- },
},
data() {
return {
searchKey: '',
labels: [],
- currentHighlightItem: -1,
localSelectedLabels: [...this.selectedLabels],
};
},
@@ -74,12 +60,6 @@ export default {
},
},
computed: {
- isDropdownVariantSidebar() {
- return this.variant === DropdownVariant.Sidebar;
- },
- isDropdownVariantEmbedded() {
- return this.variant === DropdownVariant.Embedded;
- },
labelsFetchInProgress() {
return this.$apollo.queries.labels.loading;
},
@@ -98,21 +78,11 @@ export default {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
},
- watch: {
- searchKey(value) {
- // When there is search string present
- // and there are matching results,
- // highlight first item by default.
- if (value && this.visibleLabels.length) {
- this.currentHighlightItem = 0;
- }
- },
- },
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
beforeDestroy() {
- this.$emit('closeDropdown', this.localSelectedLabels);
+ this.$emit('setLabels', this.localSelectedLabels);
this.debouncedSearchKeyUpdate.cancel();
},
methods: {
@@ -150,33 +120,6 @@ export default {
});
}
},
- /**
- * This method enables keyboard navigation support for
- * the dropdown.
- */
- handleKeyDown(e) {
- if (e.keyCode === UP_KEY_CODE && this.currentHighlightItem > 0) {
- this.currentHighlightItem -= 1;
- } else if (
- e.keyCode === DOWN_KEY_CODE &&
- this.currentHighlightItem < this.visibleLabels.length - 1
- ) {
- this.currentHighlightItem += 1;
- } else if (e.keyCode === ENTER_KEY_CODE && this.currentHighlightItem > -1) {
- this.updateSelectedLabels(this.visibleLabels[this.currentHighlightItem]);
- this.searchKey = '';
- } else if (e.keyCode === ESC_KEY_CODE) {
- this.$emit('closeDropdown', this.localSelectedLabels);
- }
-
- if (e.keyCode !== ESC_KEY_CODE) {
- // Scroll the list only after highlighting
- // styles are rendered completely.
- this.$nextTick(() => {
- this.scrollIntoViewIfNeeded();
- });
- }
- },
handleLabelClick(label) {
this.updateSelectedLabels(label);
if (!this.allowMultiselect) {
@@ -191,69 +134,41 @@ export default {
</script>
<template>
- <div
- class="labels-select-contents-list js-labels-list"
- data-testid="dropdown-wrapper"
- @keydown="handleKeyDown"
- >
- <div class="dropdown-input" @click.stop="() => {}">
- <gl-search-box-by-type
- ref="searchInput"
- :value="searchKey"
- :disabled="labelsFetchInProgress"
- data-qa-selector="dropdown_input_field"
- data-testid="dropdown-input-field"
- @input="debouncedSearchKeyUpdate"
- />
- </div>
- <div ref="labelsListContainer" class="dropdown-content" data-testid="dropdown-content">
+ <gl-dropdown-form class="labels-select-contents-list js-labels-list">
+ <gl-search-box-by-type
+ ref="searchInput"
+ :value="searchKey"
+ :disabled="labelsFetchInProgress"
+ data-qa-selector="dropdown_input_field"
+ data-testid="dropdown-input-field"
+ @input="debouncedSearchKeyUpdate"
+ />
+ <div ref="labelsListContainer" data-testid="dropdown-content">
<gl-loading-icon
v-if="labelsFetchInProgress"
class="labels-fetch-loading gl-align-items-center gl-w-full gl-h-full"
size="md"
/>
- <ul v-else class="list-unstyled gl-mb-0 gl-word-break-word" data-testid="labels-list">
- <label-item
- v-for="(label, index) in visibleLabels"
+ <template v-else>
+ <gl-dropdown-item
+ v-for="label in visibleLabels"
:key="label.id"
- :label="label"
- :is-label-set="isLabelSelected(label)"
- :highlight="index === currentHighlightItem"
- @clickLabel="handleLabelClick(label)"
- />
- <li
+ :is-checked="isLabelSelected(label)"
+ :is-check-centered="true"
+ :is-check-item="true"
+ data-testid="labels-list"
+ @click.native.capture.stop="handleLabelClick(label)"
+ >
+ <label-item :label="label" />
+ </gl-dropdown-item>
+ <gl-dropdown-item
v-show="showNoMatchingResultsMessage"
class="gl-p-3 gl-text-center"
data-testid="no-results"
>
{{ __('No matching results') }}
- </li>
- </ul>
- </div>
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-footer"
- data-testid="dropdown-footer"
- >
- <ul class="list-unstyled">
- <li v-if="allowLabelCreate">
- <gl-link
- class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
- data-testid="create-label-button"
- @click="$emit('toggleDropdownContentsCreateView')"
- >
- {{ footerCreateLabelTitle }}
- </gl-link>
- </li>
- <li>
- <gl-link
- :href="labelsManagePath"
- class="gl-display-flex gl-flex-direction-row gl-w-full gl-overflow-break-word label-item"
- >
- {{ footerManageLabelTitle }}
- </gl-link>
- </li>
- </ul>
+ </gl-dropdown-item>
+ </template>
</div>
- </div>
+ </gl-dropdown-form>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
deleted file mode 100644
index 46edfa1c42a..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-
-export default {
- components: {
- GlButton,
- GlLoadingIcon,
- },
- props: {
- labelsSelectInProgress: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- ...mapState(['allowLabelEdit', 'labelsFetchInProgress']),
- },
- methods: {
- ...mapActions(['toggleDropdownContents']),
- },
-};
-</script>
-
-<template>
- <div class="title hide-collapsed gl-mb-3">
- {{ __('Labels') }}
- <template v-if="allowLabelEdit">
- <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
- <gl-button
- category="tertiary"
- size="small"
- class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
- data-qa-selector="labels_edit_button"
- @click="toggleDropdownContents"
- >{{ __('Edit') }}</gl-button
- >
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index 58a940bca3b..71d3d87cce5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -1,5 +1,6 @@
<script>
import { GlLabel } from '@gitlab/ui';
+import { sortBy } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
@@ -7,6 +8,7 @@ export default {
components: {
GlLabel,
},
+ inject: ['allowScopedLabels'],
props: {
disableLabels: {
type: Boolean,
@@ -21,10 +23,6 @@ export default {
type: Boolean,
required: true,
},
- allowScopedLabels: {
- type: Boolean,
- required: true,
- },
labelsFilterBasePath: {
type: String,
required: true,
@@ -34,6 +32,11 @@ export default {
required: true,
},
},
+ computed: {
+ sortedSelectedLabels() {
+ return sortBy(this.selectedLabels, (label) => (isScopedLabel(label) ? 0 : 1));
+ },
+ },
methods: {
labelFilterUrl(label) {
return `${this.labelsFilterBasePath}?${this.labelsFilterParam}[]=${encodeURIComponent(
@@ -63,7 +66,7 @@ export default {
</span>
<template v-else>
<gl-label
- v-for="label in selectedLabels"
+ v-for="label in sortedSelectedLabels"
:key="label.id"
data-qa-selector="selected_label_content"
:data-qa-label-name="label.title"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
index 9aa4f5d165e..eb478645a03 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
@@ -6,9 +6,7 @@ mutation createLabel($title: String!, $color: String, $projectPath: ID, $groupPa
id
color
description
- descriptionHtml
title
- textColor
}
errors
}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
index e8fdf4bb0c2..f27f0b4e34c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
@@ -1,82 +1,21 @@
<script>
-import { GlLink, GlIcon } from '@gitlab/ui';
-
export default {
- functional: true,
props: {
label: {
type: Object,
required: true,
},
- isLabelSet: {
- type: Boolean,
- required: true,
- },
- highlight: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- render(h, { props, listeners }) {
- const { label, highlight, isLabelSet } = props;
-
- const labelColorBox = h('span', {
- class: 'dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3',
- style: {
- backgroundColor: label.color,
- },
- attrs: {
- 'data-testid': 'label-color-box',
- },
- });
-
- const checkedIcon = h(GlIcon, {
- class: {
- 'gl-mr-3 gl-flex-shrink-0': true,
- hidden: !isLabelSet,
- },
- props: {
- name: 'mobile-issue-close',
- },
- });
-
- const noIcon = h('span', {
- class: {
- 'gl-mr-5 gl-pr-3': true,
- hidden: isLabelSet,
- },
- attrs: {
- 'data-testid': 'no-icon',
- },
- });
-
- const labelTitle = h('span', label.title);
-
- const labelLink = h(
- GlLink,
- {
- class: 'gl-display-flex gl-align-items-center label-item gl-text-black-normal',
- on: {
- click: () => {
- listeners.clickLabel(label);
- },
- },
- },
- [noIcon, checkedIcon, labelColorBox, labelTitle],
- );
-
- return h(
- 'li',
- {
- class: {
- 'gl-display-block': true,
- 'gl-text-left': true,
- 'is-focused': highlight,
- },
- },
- [labelLink],
- );
},
};
</script>
+
+<template>
+ <div>
+ <span
+ class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
+ :style="{ 'background-color': label.color }"
+ data-testid="label-color-box"
+ ></span>
+ <span>{{ label.title }}</span>
+ </div>
+</template>
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 0499dfe468f..3c834770563 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
@@ -1,57 +1,40 @@
<script>
-import $ from 'jquery';
import Vue from 'vue';
-import Vuex, { mapState, mapActions, mapGetters } from 'vuex';
-import { isInViewport } from '~/lib/utils/common_utils';
+import Vuex from 'vuex';
import { __ } from '~/locale';
-
+import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import { DropdownVariant } from './constants';
-import DropdownButton from './dropdown_button.vue';
import DropdownContents from './dropdown_contents.vue';
-import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import issueLabelsQuery from './graphql/issue_labels.query.graphql';
-import labelsSelectModule from './store';
+import {
+ isDropdownVariantSidebar,
+ isDropdownVariantStandalone,
+ isDropdownVariantEmbedded,
+} from './utils';
Vue.use(Vuex);
export default {
- store: new Vuex.Store(labelsSelectModule()),
components: {
- DropdownTitle,
DropdownValue,
- DropdownButton,
DropdownContents,
DropdownValueCollapsed,
+ SidebarEditableItem,
},
- inject: ['iid', 'projectPath'],
+ inject: ['iid', 'projectPath', 'allowLabelEdit'],
props: {
allowLabelRemove: {
type: Boolean,
required: false,
default: false,
},
- allowLabelEdit: {
- type: Boolean,
- required: false,
- default: false,
- },
- allowLabelCreate: {
- type: Boolean,
- required: false,
- default: false,
- },
allowMultiselect: {
type: Boolean,
required: false,
default: false,
},
- allowScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
variant: {
type: String,
required: false,
@@ -67,16 +50,6 @@ export default {
required: false,
default: false,
},
- labelsFetchPath: {
- type: String,
- required: false,
- default: '',
- },
- labelsManagePath: {
- type: String,
- required: false,
- default: '',
- },
labelsFilterBasePath: {
type: String,
required: false,
@@ -138,149 +111,25 @@ export default {
},
},
},
- computed: {
- ...mapState(['showDropdownButton', 'showDropdownContents']),
- ...mapGetters([
- 'isDropdownVariantSidebar',
- 'isDropdownVariantStandalone',
- 'isDropdownVariantEmbedded',
- ]),
- dropdownButtonVisible() {
- return this.isDropdownVariantSidebar ? this.showDropdownButton : true;
- },
- },
- watch: {
- selectedLabels(selectedLabels) {
- this.setInitialState({
- selectedLabels,
- });
- },
- showDropdownContents(showDropdownContents) {
- this.setContentIsOnViewport(showDropdownContents);
- },
- isEditing(newVal) {
- if (newVal) {
- this.toggleDropdownContents();
- }
- },
- },
- mounted() {
- this.setInitialState({
- variant: this.variant,
- allowLabelRemove: this.allowLabelRemove,
- allowLabelEdit: this.allowLabelEdit,
- allowLabelCreate: this.allowLabelCreate,
- allowMultiselect: this.allowMultiselect,
- allowScopedLabels: this.allowScopedLabels,
- dropdownButtonText: this.dropdownButtonText,
- selectedLabels: this.selectedLabels,
- labelsFetchPath: this.labelsFetchPath,
- labelsManagePath: this.labelsManagePath,
- labelsFilterBasePath: this.labelsFilterBasePath,
- labelsFilterParam: this.labelsFilterParam,
- labelsListTitle: this.labelsListTitle,
- footerCreateLabelTitle: this.footerCreateLabelTitle,
- footerManageLabelTitle: this.footerManageLabelTitle,
- });
-
- this.$store.subscribeAction({
- after: this.handleVuexActionDispatch,
- });
-
- document.addEventListener('mousedown', this.handleDocumentMousedown);
- document.addEventListener('click', this.handleDocumentClick);
- },
- beforeDestroy() {
- document.removeEventListener('mousedown', this.handleDocumentMousedown);
- document.removeEventListener('click', this.handleDocumentClick);
- },
methods: {
- ...mapActions(['setInitialState', 'toggleDropdownContents']),
- /**
- * This method stores a mousedown event's target.
- * Required by the click listener because the click
- * event itself has no reference to this element.
- */
- handleDocumentMousedown({ target }) {
- this.mousedownTarget = target;
- },
- /**
- * This method listens for document-wide click event
- * and toggle dropdown if user clicks anywhere outside
- * the dropdown while dropdown is visible.
- */
- handleDocumentClick({ target }) {
- // We also perform the toggle exception check for the
- // last mousedown event's target to avoid hiding the
- // box when the mousedown happened inside the box and
- // only the mouseup did not.
- if (
- this.showDropdownContents &&
- !this.preventDropdownToggleOnClick(target) &&
- !this.preventDropdownToggleOnClick(this.mousedownTarget)
- ) {
- this.toggleDropdownContents();
- }
- },
- /**
- * This method checks whether a given click target
- * should prevent the dropdown from being toggled.
- */
- preventDropdownToggleOnClick(target) {
- // This approach of element detection is needed
- // as the dropdown wrapper is not using `GlDropdown` as
- // it will also require us to use `BDropdownForm`
- // which is yet to be implemented in GitLab UI.
- const hasExceptionClass = [
- 'js-dropdown-button',
- 'js-btn-cancel-create',
- 'js-sidebar-dropdown-toggle',
- ].some(
- (className) =>
- target?.classList.contains(className) ||
- target?.parentElement?.classList.contains(className),
- );
-
- const hasExceptionParent = ['.js-btn-back', '.js-labels-list'].some(
- (className) => $(target).parents(className).length,
- );
-
- const isInDropdownButtonCollapsed = this.$refs.dropdownButtonCollapsed?.$el.contains(target);
-
- const isInDropdownContents = this.$refs.dropdownContents?.$el.contains(target);
-
- return (
- hasExceptionClass ||
- hasExceptionParent ||
- isInDropdownButtonCollapsed ||
- isInDropdownContents
- );
- },
handleDropdownClose(labels) {
- // Only emit label updates if there are any labels to update
- // on UI.
- if (this.showDropdownContents) {
- this.toggleDropdownContents();
- }
if (labels.length) this.$emit('updateSelectedLabels', labels);
this.$emit('onDropdownClose');
},
+ collapseDropdown() {
+ this.$refs.editable.collapse();
+ },
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
- setContentIsOnViewport(showDropdownContents) {
- if (!showDropdownContents) {
- this.contentIsOnViewport = true;
-
- return;
- }
-
+ showDropdown() {
this.$nextTick(() => {
- if (this.$refs.dropdownContents) {
- this.contentIsOnViewport = isInViewport(this.$refs.dropdownContents.$el);
- }
+ this.$refs.dropdownContents.showDropdown();
});
},
+ isDropdownVariantSidebar,
+ isDropdownVariantStandalone,
+ isDropdownVariantEmbedded,
},
};
</script>
@@ -289,58 +138,63 @@ export default {
<div
class="labels-select-wrapper position-relative"
:class="{
- 'is-standalone': isDropdownVariantStandalone,
- 'is-embedded': isDropdownVariantEmbedded,
+ 'is-standalone': isDropdownVariantStandalone(variant),
+ 'is-embedded': isDropdownVariantEmbedded(variant),
}"
>
- <template v-if="isDropdownVariantSidebar">
+ <template v-if="isDropdownVariantSidebar(variant)">
<dropdown-value-collapsed
ref="dropdownButtonCollapsed"
:labels="issueLabels"
@onValueClick="handleCollapsedValueClick"
/>
- <dropdown-title
- :allow-label-edit="allowLabelEdit"
- :labels-select-in-progress="labelsSelectInProgress"
- />
- <dropdown-value
- :disable-labels="labelsSelectInProgress"
- :selected-labels="issueLabels"
- :allow-label-remove="allowLabelRemove"
- :allow-scoped-labels="allowScopedLabels"
- :labels-filter-base-path="labelsFilterBasePath"
- :labels-filter-param="labelsFilterParam"
- @onLabelRemove="$emit('onLabelRemove', $event)"
+ <sidebar-editable-item
+ ref="editable"
+ :title="__('Labels')"
+ :loading="labelsSelectInProgress"
+ :can-edit="allowLabelEdit"
+ @open="showDropdown"
>
- <slot></slot>
- </dropdown-value>
- <dropdown-button v-show="dropdownButtonVisible" class="gl-mt-2" />
- <dropdown-contents
- v-if="dropdownButtonVisible && showDropdownContents"
- ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
- :render-on-top="!contentIsOnViewport"
- :labels-create-title="labelsCreateTitle"
- :selected-labels="selectedLabels"
- @closeDropdown="handleDropdownClose"
- />
- </template>
- <template v-if="isDropdownVariantStandalone || isDropdownVariantEmbedded">
- <dropdown-button v-show="dropdownButtonVisible" />
- <dropdown-contents
- v-if="dropdownButtonVisible && showDropdownContents"
- ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
- :render-on-top="!contentIsOnViewport"
- :selected-labels="selectedLabels"
- @closeDropdown="handleDropdownClose"
- />
+ <template #collapsed>
+ <dropdown-value
+ :disable-labels="labelsSelectInProgress"
+ :selected-labels="issueLabels"
+ :allow-label-remove="allowLabelRemove"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
+ @onLabelRemove="$emit('onLabelRemove', $event)"
+ >
+ <slot></slot>
+ </dropdown-value>
+ </template>
+ <template #default="{ edit }">
+ <dropdown-value
+ :disable-labels="labelsSelectInProgress"
+ :selected-labels="issueLabels"
+ :allow-label-remove="allowLabelRemove"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
+ class="gl-mb-2"
+ @onLabelRemove="$emit('onLabelRemove', $event)"
+ >
+ <slot></slot>
+ </dropdown-value>
+ <dropdown-contents
+ v-if="edit"
+ ref="dropdownContents"
+ :dropdown-button-text="dropdownButtonText"
+ :allow-multiselect="allowMultiselect"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ :labels-create-title="labelsCreateTitle"
+ :selected-labels="selectedLabels"
+ :variant="variant"
+ @closeDropdown="collapseDropdown"
+ @setLabels="handleDropdownClose"
+ />
+ </template>
+ </sidebar-editable-item>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
deleted file mode 100644
index b3d4a204a81..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as types from './mutation_types';
-
-export const setInitialState = ({ commit }, props) => commit(types.SET_INITIAL_STATE, props);
-
-export const toggleDropdownButton = ({ commit }) => commit(types.TOGGLE_DROPDOWN_BUTTON);
-export const toggleDropdownContents = ({ commit }) => commit(types.TOGGLE_DROPDOWN_CONTENTS);
-
-export const toggleDropdownContentsCreateView = ({ commit }) =>
- commit(types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW);
-
-export const updateSelectedLabels = ({ commit }, labels) =>
- commit(types.UPDATE_SELECTED_LABELS, { labels });
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js
deleted file mode 100644
index d14f96720b7..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/getters.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { __, s__, sprintf } from '~/locale';
-import { DropdownVariant } from '../constants';
-
-/**
- * Returns string representing current labels
- * selection on dropdown button.
- *
- * @param {object} state
- */
-export const dropdownButtonText = (state, getters) => {
- const selectedLabels = getters.isDropdownVariantSidebar
- ? state.labels.filter((label) => label.set)
- : state.selectedLabels;
-
- if (!selectedLabels.length) {
- return state.dropdownButtonText || __('Label');
- } else if (selectedLabels.length > 1) {
- return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
- firstLabelName: selectedLabels[0].title,
- remainingLabelCount: selectedLabels.length - 1,
- });
- }
- return selectedLabels[0].title;
-};
-
-/**
- * Returns array containing only label IDs from
- * selectedLabels array.
- * @param {object} state
- */
-export const selectedLabelsList = (state) => state.selectedLabels.map((label) => label.id);
-
-/**
- * Returns boolean representing whether dropdown variant
- * is `sidebar`
- * @param {object} state
- */
-export const isDropdownVariantSidebar = (state) => state.variant === DropdownVariant.Sidebar;
-
-/**
- * Returns boolean representing whether dropdown variant
- * is `standalone`
- * @param {object} state
- */
-export const isDropdownVariantStandalone = (state) => state.variant === DropdownVariant.Standalone;
-
-/**
- * Returns boolean representing whether dropdown variant
- * is `embedded`
- * @param {object} state
- */
-export const isDropdownVariantEmbedded = (state) => state.variant === DropdownVariant.Embedded;
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js
deleted file mode 100644
index 5f61cb732c8..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-export default () => ({
- namespaced: true,
- state: state(),
- actions,
- getters,
- mutations,
-});
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
deleted file mode 100644
index bd71c3b85f1..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutation_types.js
+++ /dev/null
@@ -1,8 +0,0 @@
-export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
-
-export const TOGGLE_DROPDOWN_BUTTON = 'TOGGLE_DROPDOWN_VISIBILITY';
-export const TOGGLE_DROPDOWN_CONTENTS = 'TOGGLE_DROPDOWN_CONTENTS';
-
-export const UPDATE_SELECTED_LABELS = 'UPDATE_SELECTED_LABELS';
-
-export const TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW = 'TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
deleted file mode 100644
index 45ec4d7ae04..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
-import { DropdownVariant } from '../constants';
-import * as types from './mutation_types';
-
-export default {
- [types.SET_INITIAL_STATE](state, props) {
- Object.assign(state, { ...props });
- },
-
- [types.TOGGLE_DROPDOWN_BUTTON](state) {
- state.showDropdownButton = !state.showDropdownButton;
- },
-
- [types.TOGGLE_DROPDOWN_CONTENTS](state) {
- if (state.variant === DropdownVariant.Sidebar) {
- state.showDropdownButton = !state.showDropdownButton;
- }
- state.showDropdownContents = !state.showDropdownContents;
- // Ensure that Create View is hidden by default
- // when dropdown contents are revealed.
- if (state.showDropdownContents) {
- state.showDropdownContentsCreateView = false;
- }
- },
-
- [types.TOGGLE_DROPDOWN_CONTENTS_CREATE_VIEW](state) {
- state.showDropdownContentsCreateView = !state.showDropdownContentsCreateView;
- },
- [types.UPDATE_SELECTED_LABELS](state, { labels }) {
- // Find the label to update from all the labels
- // and change `set` prop value to represent their current state.
- const labelId = labels.pop()?.id;
- const candidateLabel = state.labels.find((label) => labelId === label.id);
- if (candidateLabel) {
- candidateLabel.touched = true;
- candidateLabel.set = !candidateLabel.set;
- }
-
- if (isScopedLabel(candidateLabel)) {
- const scopedBase = scopedLabelKey(candidateLabel);
- const currentActiveScopedLabel = state.labels.find(
- ({ title }) => title.indexOf(scopedBase) === 0 && title !== candidateLabel.title,
- );
-
- if (currentActiveScopedLabel) {
- currentActiveScopedLabel.set = false;
- }
- }
- },
-};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
deleted file mode 100644
index 220bab05ed2..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/state.js
+++ /dev/null
@@ -1,28 +0,0 @@
-export default () => ({
- // Initial Data
- labels: [],
- selectedLabels: [],
- labelsListTitle: '',
- footerCreateLabelTitle: '',
- footerManageLabelTitle: '',
- dropdownButtonText: '',
-
- // Paths
- namespace: '',
- labelsFetchPath: '',
- labelsFilterBasePath: '',
-
- // UI Flags
- variant: '',
- allowLabelRemove: false,
- allowLabelCreate: false,
- allowLabelEdit: false,
- allowScopedLabels: false,
- allowMultiselect: false,
- showDropdownButton: false,
- showDropdownContents: false,
- showDropdownContentsCreateView: false,
- labelsFetchInProgress: false,
- labelCreateInProgress: false,
- selectedLabelsUpdated: false,
-});
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js
new file mode 100644
index 00000000000..b5cd946a189
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js
@@ -0,0 +1,22 @@
+import { DropdownVariant } from './constants';
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `sidebar`
+ * @param {string} variant
+ */
+export const isDropdownVariantSidebar = (variant) => variant === DropdownVariant.Sidebar;
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `standalone`
+ * @param {string} variant
+ */
+export const isDropdownVariantStandalone = (variant) => variant === DropdownVariant.Standalone;
+
+/**
+ * Returns boolean representing whether dropdown variant
+ * is `embedded`
+ * @param {string} variant
+ */
+export const isDropdownVariantEmbedded = (variant) => variant === DropdownVariant.Embedded;
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
new file mode 100644
index 00000000000..00aa5519ec6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.stories.js
@@ -0,0 +1,38 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+import '@gitlab/ui/dist/utility_classes.css';
+import UsageGraph from './usage_graph.vue';
+
+export default {
+ component: UsageGraph,
+ title: 'vue_shared/components/storage_counter/usage_graph',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { UsageGraph },
+ props: Object.keys(argTypes),
+ template: '<usage-graph v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.argTypes = {
+ rootStorageStatistics: {
+ description: 'The statistics object with all its fields',
+ type: { name: 'object', required: true },
+ defaultValue: {
+ buildArtifactsSize: 400000,
+ pipelineArtifactsSize: 38000,
+ lfsObjectsSize: 4800000,
+ packagesSize: 3800000,
+ repositorySize: 39000000,
+ snippetsSize: 2000112,
+ storageSize: 39930000,
+ uploadsSize: 7000,
+ wikiSize: 300000,
+ },
+ },
+ limit: {
+ description:
+ 'When a limit is set, users will see how much of their storage usage (limit) is used. In case the limit is 0 or the current usage exceeds the limit, it just renders the distribution',
+ defaultValue: 0,
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
new file mode 100644
index 00000000000..c33d065ff4b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/storage_counter/usage_graph.vue
@@ -0,0 +1,148 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ rootStorageStatistics: {
+ required: true,
+ type: Object,
+ },
+ limit: {
+ required: true,
+ type: Number,
+ },
+ },
+ computed: {
+ storageTypes() {
+ const {
+ buildArtifactsSize,
+ pipelineArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ repositorySize,
+ storageSize,
+ wikiSize,
+ snippetsSize,
+ uploadsSize,
+ } = this.rootStorageStatistics;
+ const artifactsSize = buildArtifactsSize + pipelineArtifactsSize;
+
+ if (storageSize === 0) {
+ return null;
+ }
+
+ return [
+ {
+ name: s__('UsageQuota|Repositories'),
+ style: this.usageStyle(this.barRatio(repositorySize)),
+ class: 'gl-bg-data-viz-blue-500',
+ size: repositorySize,
+ },
+ {
+ name: s__('UsageQuota|LFS Objects'),
+ style: this.usageStyle(this.barRatio(lfsObjectsSize)),
+ class: 'gl-bg-data-viz-orange-600',
+ size: lfsObjectsSize,
+ },
+ {
+ name: s__('UsageQuota|Packages'),
+ style: this.usageStyle(this.barRatio(packagesSize)),
+ class: 'gl-bg-data-viz-aqua-500',
+ size: packagesSize,
+ },
+ {
+ name: s__('UsageQuota|Artifacts'),
+ style: this.usageStyle(this.barRatio(artifactsSize)),
+ class: 'gl-bg-data-viz-green-600',
+ size: artifactsSize,
+ tooltip: s__('UsageQuota|Artifacts is a sum of build and pipeline artifacts.'),
+ },
+ {
+ name: s__('UsageQuota|Wikis'),
+ style: this.usageStyle(this.barRatio(wikiSize)),
+ class: 'gl-bg-data-viz-magenta-500',
+ size: wikiSize,
+ },
+ {
+ name: s__('UsageQuota|Snippets'),
+ style: this.usageStyle(this.barRatio(snippetsSize)),
+ class: 'gl-bg-data-viz-orange-800',
+ size: snippetsSize,
+ },
+ {
+ name: s__('UsageQuota|Uploads'),
+ style: this.usageStyle(this.barRatio(uploadsSize)),
+ class: 'gl-bg-data-viz-aqua-700',
+ size: uploadsSize,
+ },
+ ]
+ .filter((data) => data.size !== 0)
+ .sort((a, b) => b.size - a.size);
+ },
+ },
+ methods: {
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ usageStyle(ratio) {
+ return { flex: ratio };
+ },
+ barRatio(size) {
+ let max = this.rootStorageStatistics.storageSize;
+
+ if (this.limit !== 0 && max <= this.limit) {
+ max = this.limit;
+ }
+
+ return size / max;
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100">
+ <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex">
+ <div
+ v-for="storageType in storageTypes"
+ :key="storageType.name"
+ class="storage-type-usage gl-h-full gl-display-inline-block"
+ :class="storageType.class"
+ :style="storageType.style"
+ data-testid="storage-type-usage"
+ ></div>
+ </div>
+ <div class="row py-0">
+ <div
+ v-for="storageType in storageTypes"
+ :key="storageType.name"
+ class="col-md-auto gl-display-flex gl-align-items-center"
+ data-testid="storage-type-legend"
+ >
+ <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div>
+ <span class="gl-mr-2 gl-font-weight-bold gl-font-sm">
+ {{ storageType.name }}
+ </span>
+ <span class="gl-text-gray-500 gl-font-sm">
+ {{ formatSize(storageType.size) }}
+ </span>
+ <span
+ v-if="storageType.tooltip"
+ v-gl-tooltip
+ :title="storageType.tooltip"
+ :aria-label="storageType.tooltip"
+ class="gl-ml-2"
+ >
+ <gl-icon name="question" :size="12" />
+ </span>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
index b9ee74d6a03..42334d80eec 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown.vue
@@ -66,7 +66,7 @@ export default {
};
</script>
<template>
- <gl-dropdown :text="selectedTimezoneLabel" block lazy menu-class="gl-w-full!">
+ <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"
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 f387f8ca128..74616763f8f 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
@@ -1,6 +1,12 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlPopover, GlLink, GlSkeletonLoader, GlIcon } from '@gitlab/ui';
+import {
+ GlPopover,
+ GlLink,
+ GlSkeletonLoader,
+ GlIcon,
+ GlSafeHtmlDirective,
+ GlSprintf,
+} from '@gitlab/ui';
import UserNameWithStatus from '~/sidebar/components/assignees/user_name_with_status.vue';
import { glEmojiTag } from '../../../emoji';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
@@ -17,6 +23,10 @@ export default {
GlSkeletonLoader,
UserAvatarImage,
UserNameWithStatus,
+ GlSprintf,
+ },
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
},
props: {
target: {
@@ -50,6 +60,7 @@ export default {
return this.user?.status?.availability || '';
},
},
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
};
</script>
@@ -83,7 +94,7 @@ export default {
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
<gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
- <span ref="bio" class="gl-ml-2 gl-overflow-hidden" v-html="user.bioHtml"></span>
+ <span ref="bio" class="gl-ml-2 gl-overflow-hidden">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
<gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
@@ -95,12 +106,14 @@ export default {
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status gl-mt-3">
- <span v-html="statusHtml"></span>
+ <span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
</div>
<div v-if="user.bot" class="gl-text-blue-500">
<gl-icon name="question" />
<gl-link data-testid="user-popover-bot-docs-link" :href="user.websiteUrl">
- {{ sprintf(__('Learn more about %{username}'), { username: user.name }) }}
+ <gl-sprintf :message="__('Learn more about %{username}')">
+ <template #username>{{ user.name }}</template>
+ </gl-sprintf>
</gl-link>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index 4a50dfbd82f..b024e92bd0e 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -24,6 +24,7 @@ export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning';
export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
+export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management';
export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning';
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js
index e860e3af924..c1b3f546431 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/state.js
@@ -1,7 +1,5 @@
export default () => ({
paths: {
- head: null,
- base: null,
diffEndpoint: null,
},
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js
index e860e3af924..c1b3f546431 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/state.js
@@ -1,7 +1,5 @@
export default () => ({
paths: {
- head: null,
- base: null,
diffEndpoint: null,
},
diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js
index 8cd1d2eb2ca..55ac2f0be6a 100644
--- a/app/assets/javascripts/webpack_non_compiled_placeholder.js
+++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js
@@ -8,7 +8,7 @@ Object.assign(div.style, {
left: 0,
'z-index': 100000,
background: 'rgba(0,0,0,0.9)',
- 'font-size': '25px',
+ 'font-size': '20px',
'font-family': 'monospace',
color: 'white',
padding: '2.5em',
@@ -16,9 +16,23 @@ Object.assign(div.style, {
});
div.innerHTML = `
-<h1 style="color:white">🧙 Webpack is doing its magic 🧙</h1>
-<p>If you use Hot Module reloading, the page will reload in a few seconds.</p>
-<p>If you do not use Hot Module reloading, please <a href="">reload the page manually in a few seconds</a></p>
+<!-- https://github.com/webpack/media/blob/master/logo/icon-square-big.svg -->
+<svg height="50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200">
+ <path fill="#FFF" d="M600 0l530.3 300v600L600 1200 69.7 900V300z"/>
+ <path fill="#8ED6FB" class="st1" d="M1035.6 879.3l-418.1 236.5V931.6L878 788.3l157.6 91zm28.6-25.9V358.8l-153 88.3V765l153 88.4zm-901.5 25.9l418.1 236.5V931.6L320.3 788.3l-157.6 91zm-28.6-25.9V358.8l153 88.3V765l-153 88.4zM152 326.8L580.8 84.2v178.1L306.1 413.4l-2.1 1.2-152-87.8zm894.3 0L617.5 84.2v178.1l274.7 151.1 2.1 1.2 152-87.8z"/>
+ <path fill="#1C78C0" d="M580.8 889.7l-257-141.3v-280l257 148.4v272.9zm36.7 0l257-141.3v-280l-257 148.4v272.9zm-18.3-283.6zM341.2 436l258-141.9 258 141.9-258 149-258-149z"/>
+</svg>
+
+<h1 style="color:white">✨ webpack is compiling frontend assets ✨</h1>
+<p>
+ To reduce GDK memory consumption, incremental on-demand compiling is on by default.<br />
+ You can disable this within gdk.yml.
+ Learn more <a href="https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/configuration.md#webpack-settings">here</a>.
+</p>
+<p>
+ If you have live_reload enabled, the page will reload automatically when complete.<br />
+ Otherwise, please <a href="">reload the page manually in a few seconds</a>
+</p>
`;
document.body.append(div);
diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue
index 11096b08032..a93bda326de 100644
--- a/app/assets/javascripts/whats_new/components/feature.vue
+++ b/app/assets/javascripts/whats_new/components/feature.vue
@@ -40,7 +40,7 @@ export default {
:href="feature.url"
target="_blank"
class="gl-display-block"
- data-track-event="click_whats_new_item"
+ data-track-action="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
@@ -55,7 +55,7 @@ export default {
:href="feature.url"
target="_blank"
class="whats-new-item-title-link gl-display-block gl-mt-4 gl-mb-1"
- data-track-event="click_whats_new_item"
+ data-track-action="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
@@ -79,7 +79,7 @@ export default {
<gl-button
:href="feature.url"
target="_blank"
- data-track-event="click_whats_new_item"
+ data-track-action="click_whats_new_item"
:data-track-label="feature.title"
:data-track-property="feature.url"
>
diff --git a/app/assets/javascripts/work_items/components/app.vue b/app/assets/javascripts/work_items/components/app.vue
new file mode 100644
index 00000000000..93de17d1e43
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/app.vue
@@ -0,0 +1,9 @@
+<script>
+export default {
+ name: 'WorkItemRoot',
+};
+</script>
+
+<template>
+ <div></div>
+</template>
diff --git a/app/assets/javascripts/work_items/graphql/typedefs.graphql b/app/assets/javascripts/work_items/graphql/typedefs.graphql
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/typedefs.graphql
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
new file mode 100644
index 00000000000..a635d43776d
--- /dev/null
+++ b/app/assets/javascripts/work_items/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import App from './components/app.vue';
+
+export const initWorkItemsRoot = () => {
+ const el = document.querySelector('#js-work-items');
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(App);
+ },
+ });
+};
diff --git a/app/assets/stylesheets/application_dark.scss b/app/assets/stylesheets/application_dark.scss
index 7d6ccc40278..dae0cd72a8f 100644
--- a/app/assets/stylesheets/application_dark.scss
+++ b/app/assets/stylesheets/application_dark.scss
@@ -44,6 +44,17 @@ body.gl-dark {
}
}
+ .header-search {
+ background-color: var(--gray-100) !important;
+ box-shadow: inset 0 0 0 1px var(--border-color) !important;
+
+ &:active,
+ &:hover {
+ background-color: var(--gray-100) !important;
+ box-shadow: inset 0 0 0 1px var(--blue-200) !important;
+ }
+ }
+
.search {
form {
background-color: var(--gray-100);
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 64abf5574fa..a013d971efb 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -1,8 +1,10 @@
.ProseMirror {
td,
th,
- li {
- :only-child {
+ li,
+ dd,
+ dt {
+ :first-child {
margin-bottom: 0 !important;
}
}
@@ -34,6 +36,20 @@
}
}
}
+
+ .dl-content {
+ width: 100%;
+
+ > li {
+ list-style-type: none;
+ margin-left: $gl-spacing-scale-5;
+
+ &.dl-term {
+ margin: 0;
+ font-weight: 600;
+ }
+ }
+ }
}
.table-creator-grid-item {
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index f4519841ce3..dc08c816d7d 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -91,6 +91,7 @@ a {
.field {
margin-bottom: 0;
margin-right: 0.5em;
+ flex: 1;
}
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 264373451d5..a0682eabf01 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -154,12 +154,6 @@
}
}
- .cover-desc {
- &.username:last-child {
- padding-bottom: $gl-padding;
- }
- }
-
.cover-controls {
@include media-breakpoint-up(sm) {
position: absolute;
@@ -343,8 +337,6 @@
}
.code-block {
- background: $black;
- color: $gray-darkest;
white-space: pre;
overflow-x: auto;
font-size: 12px;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 61a20c7a8fd..568182ad796 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -613,7 +613,7 @@ table.code {
grid-template-columns: 1fr 1fr;
}
- &.inline {
+ &.inline-diff-view {
.diff-grid-comments {
display: grid;
grid-template-columns: 1fr;
@@ -682,26 +682,6 @@ table.code {
max-height: 50vh;
}
-.diff-stats-summary-toggler {
- padding: 0;
- background-color: transparent;
- border: 0;
- color: $blue-600;
- font-weight: $gl-font-weight-bold;
-
- &:hover,
- &:focus {
- outline: none;
- color: $blue-800;
- }
-
- .caret-icon {
- position: relative;
- top: 2px;
- left: -1px;
- }
-}
-
// Mobile
@media (max-width: 480px) {
.diff-title {
@@ -853,21 +833,14 @@ table.code {
.diff-files-changed {
.inline-parallel-buttons {
- position: relative;
+ @include gl-relative;
z-index: 1;
}
- .commit-stat-summary {
- @include media-breakpoint-up(sm) {
- background-color: $white;
- }
- }
-
@include media-breakpoint-up(sm) {
- position: -webkit-sticky;
- position: sticky;
+ @include gl-sticky;
top: $header-height + $mr-tabs-height;
- background-color: $white;
+ @include gl-bg-white;
z-index: 200;
.with-performance-bar & {
@@ -875,14 +848,13 @@ table.code {
}
&.is-stuck {
- padding-top: 0;
- padding-bottom: 0;
+ @include gl-py-0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
.diff-stats-additions-deletions-expanded,
.inline-parallel-buttons {
- display: none !important;
+ @include gl-display-none;
}
}
}
@@ -890,12 +862,13 @@ table.code {
@include media-breakpoint-up(lg) {
&.is-stuck {
.diff-stats-additions-deletions-collapsed {
- display: block !important;
+ @include gl-display-block;
}
}
}
}
+
.diff-file-changes {
max-width: 560px;
width: 100%;
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index f76101d92b1..5dd71cec8d1 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -386,15 +386,6 @@
}
}
}
-
- .boards-add-list > .btn {
- text-align: left;
-
- > svg {
- position: absolute;
- right: 6px;
- }
- }
}
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 222e10f51ad..0aeb7208c59 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -36,6 +36,7 @@
}
}
+.ci-status-icon-notification,
.ci-status-icon-preparing,
.ci-status-icon-created,
.ci-status-icon-skipped,
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 603b05efe10..aeb3bb2286f 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -41,6 +41,12 @@
}
}
+ .media-container {
+ display: inline-flex;
+ flex-direction: column;
+ margin-bottom: $gl-spacing-scale-2;
+ }
+
img:not(.emoji) {
margin: 0 0 8px;
}
@@ -549,17 +555,12 @@
margin: 0;
font-size: $gl-font-size-small;
}
+ }
- ul.dropdown-menu {
- margin-top: 4px;
- margin-bottom: 24px;
- padding: 8px 0;
-
- li {
- margin: 0;
- padding: 0 1px;
- }
- }
+ .gl-new-dropdown-item {
+ margin: 0;
+ padding: 0;
+ line-height: 1rem;
}
/* AsciiDoc(tor) built-in alignment roles */
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 726f8e28efe..099dfa28b9f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -505,7 +505,7 @@ $line-removed-dark: #fac5cd !default;
* would hide other layers (selected text, matching brackets).
*
* When the transparent colors get layered on white background, they create their
- * full opacity counterparts (computed with https://stackoverflow.com/a/12228643/606571):
+ * full opacity counterparts:
*
* - white + $line-added-transparent = $line-added
* - white + $line-added-transparent + $line-added-dark-transparent = $line-added-dark
diff --git a/app/assets/stylesheets/mailer.scss b/app/assets/stylesheets/mailer.scss
index 9d889f111dd..3220510775c 100644
--- a/app/assets/stylesheets/mailer.scss
+++ b/app/assets/stylesheets/mailer.scss
@@ -1,7 +1,6 @@
@import 'framework/variables';
// Do not use 3-letter hex codes, bgcolor vs css background-color is problematic in emails
-// See https://stackoverflow.com/questions/28551981/why-are-3-digit-hex-color-code-values-interpreted-differently-in-internet-explor
//
// stylelint-disable color-hex-length
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index 2f8602a212d..8794acd3c78 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -1,15 +1,3 @@
-@mixin flat-connector-before($length: 44px) {
- &::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -$length;
- border-top: 2px solid var(--border-color, $border-color);
- width: $length;
- height: 1px;
- }
-}
-
@mixin build-content($border-radius: 30px) {
display: inline-block;
padding: 8px 10px 9px;
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 10183f774b1..4806f4b054b 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -15,14 +15,6 @@
}
}
-.dropdown-menu-issues-board-new {
- width: 320px;
-
- .dropdown-content {
- max-height: 140px;
- }
-}
-
.issue-board-dropdown-content {
margin: 0;
padding: $gl-padding-4 $gl-padding $gl-padding;
@@ -256,7 +248,8 @@
margin-right: 4px;
}
- .confidential-icon {
+ .confidential-icon,
+ .hidden-icon {
color: var(--orange-500, $orange-500);
cursor: help;
}
@@ -437,6 +430,10 @@
height: $input-height;
}
+.issue-boards-content {
+ isolation: isolate;
+}
+
.issue-boards-content.is-focused {
position: fixed;
width: 100%;
diff --git a/app/assets/stylesheets/page_bundles/cycle_analytics.scss b/app/assets/stylesheets/page_bundles/cycle_analytics.scss
index 2248d95ae24..5d42ece32c9 100644
--- a/app/assets/stylesheets/page_bundles/cycle_analytics.scss
+++ b/app/assets/stylesheets/page_bundles/cycle_analytics.scss
@@ -3,293 +3,4 @@
.cycle-analytics {
margin: 24px auto 0;
position: relative;
-
- .landing {
- margin-top: 0;
-
- .inner-content {
- white-space: normal;
-
- h4,
- p {
- margin: 7px 0 0;
- max-width: 480px;
- padding: 0 $gl-padding;
-
- @include media-breakpoint-down(sm) {
- margin: 0 auto;
- }
- }
- }
-
- .svg-container svg {
- width: 136px;
- height: 136px;
- }
- }
-
- .col-headers {
- ul {
- margin: 0;
- padding: 0;
- }
-
- li {
- line-height: 50px;
- }
- }
-
- .card {
- .content-block {
- padding: 24px 0;
- border-bottom: 0;
- position: relative;
-
- @include media-breakpoint-down(xs) {
- padding: 6px 0 24px;
- }
- }
-
- .column {
- text-align: center;
-
- @include media-breakpoint-down(xs) {
- padding: 15px 0;
- }
-
- .header {
- font-size: 30px;
- line-height: 38px;
- font-weight: $gl-font-weight-normal;
- margin: 0;
- }
-
- .text {
- color: var(--gray-500, $gray-500);
- margin: 0;
- }
-
- &:last-child {
- @include media-breakpoint-down(xs) {
- text-align: center;
- }
- }
- }
- }
-
- .stage-panel-body {
- display: flex;
- flex-wrap: wrap;
- }
-
- .stage-nav,
- .stage-entries {
- display: flex;
- vertical-align: top;
- font-size: $gl-font-size;
- }
-
- .stage-nav {
- width: 40%;
- margin-bottom: 0;
-
- ul {
- padding: 0;
- margin: 0;
- width: 100%;
- }
-
- li {
- list-style-type: none;
- }
-
- .stage-nav-item {
- line-height: 65px;
-
- &.active {
- background: var(--blue-50, $blue-50);
- border-color: var(--blue-300, $blue-300);
- box-shadow: inset 4px 0 0 0 var(--blue-500, $blue-500);
- }
-
- &:hover:not(.active) {
- background-color: var(--gray-10, $gray-10);
- box-shadow: inset 2px 0 0 0 var(--border-color, $border-color);
- cursor: pointer;
- }
-
- .stage-nav-item-cell.stage-name {
- width: 44.5%;
- }
-
- .stage-nav-item-cell.stage-median {
- min-width: 43%;
- }
-
- .stage-empty,
- .not-available {
- color: var(--gray-500, $gray-500);
- }
- }
- }
-
- .stage-panel-container {
- width: 100%;
- overflow: auto;
- }
-
- .stage-panel {
- min-width: 968px;
-
- .card-header {
- padding: 0;
- background-color: transparent;
- }
-
- .events-description {
- line-height: 65px;
- }
-
- .events-info {
- color: var(--gray-500, $gray-500);
- }
- }
-
- .stage-events {
- min-height: 467px;
- }
-
- .stage-event-list {
- margin: 0;
- padding: 0;
- }
-
- .stage-event-item {
- @include clearfix;
- list-style-type: none;
- padding-bottom: $gl-padding;
- margin-bottom: $gl-padding;
- border-bottom: 1px solid var(--gray-50, $gray-50);
-
- &:last-child {
- border-bottom: 0;
- margin-bottom: 0;
- }
-
- .item-details,
- .item-time {
- float: left;
- }
-
- .item-details {
- width: 75%;
- }
-
- .item-title {
- margin: 0 0 2px;
-
- &.issue-title,
- &.commit-title,
- &.merge-request-title {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- max-width: 100%;
- display: block;
-
- a {
- color: var(--gl-text-color, $gl-text-color);
- }
- }
- }
-
- .item-time {
- width: 25%;
- text-align: right;
- }
-
- .total-time {
- font-size: $cycle-analytics-big-font;
- color: var(--gl-text-color, $gl-text-color);
-
- span {
- color: var(--gl-text-color, $gl-text-color);
- font-size: $gl-font-size;
- }
- }
-
- .issue-date,
- .build-date {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- .mr-link,
- .issue-link,
- .commit-author-link,
- .issue-author-link {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- // Custom CSS for components
- .item-conmmit-component {
- .commit-icon {
- svg {
- display: inline-block;
- width: 20px;
- height: 20px;
- vertical-align: bottom;
- }
- }
- }
-
- .merge-request-branch {
- a {
- max-width: 180px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- display: inline-block;
- vertical-align: bottom;
- }
- }
- }
-
- // Custom Styles for stage items
- .item-build-component {
- .item-title {
- .icon-build-status {
- float: left;
- margin-right: 5px;
- position: relative;
- top: 2px;
- }
-
- .item-build-name {
- color: var(--gl-text-color, $gl-text-color);
- }
-
- .pipeline-id {
- color: var(--gl-text-color, $gl-text-color);
- padding: 0 3px 0 0;
- }
-
- .ref-name {
- color: var(--gray-900, $gray-900);
- display: inline-block;
- max-width: 180px;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: nowrap;
- line-height: 1.3;
- vertical-align: top;
- }
-
- .commit-sha {
- color: var(--blue-600, $blue-600);
- line-height: 1.3;
- vertical-align: top;
- font-weight: $gl-font-weight-normal;
- }
- }
- }
}
diff --git a/app/assets/stylesheets/page_bundles/escalation_policies.scss b/app/assets/stylesheets/page_bundles/escalation_policies.scss
index f188dde1183..6f3873fea0c 100644
--- a/app/assets/stylesheets/page_bundles/escalation_policies.scss
+++ b/app/assets/stylesheets/page_bundles/escalation_policies.scss
@@ -16,9 +16,6 @@ $stroke-size: 1px;
.right-arrow {
@include gl-relative;
- @include gl-mx-5;
- @include gl-display-inline-block;
- @include gl-vertical-align-middle;
height: $stroke-size;
background-color: var(--gray-900, $gray-900);
min-width: $gl-spacing-scale-7;
@@ -27,7 +24,6 @@ $stroke-size: 1px;
@include gl-absolute;
top: -2*$stroke-size;
left: calc(100% - #{5*$stroke-size});
- @include gl-display-inline-block;
@include gl-p-1;
@include gl-border-solid;
border-width: 0 $stroke-size $stroke-size 0;
@@ -35,3 +31,24 @@ $stroke-size: 1px;
transform: rotate(-45deg);
}
}
+
+.escalation-rule-row {
+ @media (max-width: $breakpoint-lg) {
+ @include gl-flex-wrap;
+ }
+}
+
+.rule-condition {
+ @media (min-width: $breakpoint-lg) {
+ flex-basis: 25%;
+ flex-shrink: 0;
+ }
+
+ @media (max-width: $breakpoint-lg) {
+ @include gl-w-full;
+ }
+}
+
+.rule-action {
+ min-width: 0;
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 6a20ff3b3fa..28354b83856 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -40,7 +40,7 @@
position: -webkit-sticky;
position: sticky;
- // Unitless zero values are not allowed in calculations https://stackoverflow.com/a/55391061
+ // Unitless zero values are not allowed in calculations
// stylelint-disable-next-line length-zero-no-unit
top: calc(#{$top-pos} + var(--system-header-height, 0px) + var(--performance-bar-height, 0px));
// stylelint-disable-next-line length-zero-no-unit
diff --git a/app/assets/stylesheets/page_bundles/new_namespace.scss b/app/assets/stylesheets/page_bundles/new_namespace.scss
index 189f010bdb2..37a1231ec6b 100644
--- a/app/assets/stylesheets/page_bundles/new_namespace.scss
+++ b/app/assets/stylesheets/page_bundles/new_namespace.scss
@@ -17,10 +17,10 @@ $new-namespace-panel-height: 240px;
.new-namespace-panel {
&:hover {
- background-color: $gray-10;
+ background-color: var(--gray-50, $gray-10);
}
- color: $purple-700;
+ color: var(--purple-700, $purple-700);
min-height: $new-namespace-panel-height;
text-align: center;
@include media-breakpoint-up(lg) {
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index c9171eb4fc7..206c2eb09d0 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -120,17 +120,10 @@
}
}
-.pipeline-tab-content {
- display: flex;
- width: 100%;
- min-height: $dropdown-max-height-lg;
- background-color: var(--gray-50, $gray-50);
- padding: $gl-padding 0;
- overflow: auto;
-}
-// These are single-value classes to use with utility-class style CSS
-// but to still access this variable. Do not add other styles.
+// These are single-value classes to use with utility-class style CSS.
+// They are here to still access a variable or because they use magic values.
+// scoped to the graph. Do not add other styles.
.gl-pipeline-min-h {
min-height: $dropdown-max-height-lg;
}
@@ -147,22 +140,6 @@
padding-right: 120px;
}
-.gl-build-content {
- display: inline-block;
- padding: 8px 10px 9px;
- width: 100%;
- border: 1px solid var(--border-color, $border-color);
- border-radius: 30px;
- background-color: var(--white, $white);
-
- &:hover,
- &:focus {
- background-color: var(--gray-50, $gray-50);
- border: 1px solid $dropdown-toggle-active-border-color;
- color: var(--gl-text-color, $gl-text-color);
- }
-}
-
.gl-ci-action-icon-container {
position: absolute;
right: 5px;
@@ -180,259 +157,6 @@
}
}
-// Pipeline graph, used at
-// app/assets/javascripts/pipelines/components/graph/graph_component.vue
-.pipeline-graph {
- white-space: nowrap;
- transition: max-height 0.3s, padding 0.3s;
-
- .stage-column-list,
- .builds-container > ul {
- padding: 0;
- }
-
- a {
- text-decoration: none;
- color: var(--gl-text-color, $gl-text-color);
- }
-
- svg {
- vertical-align: middle;
- }
-
- .stage-column {
- display: inline-block;
- vertical-align: top;
-
- &.left-margin {
- &:not(:first-child) {
- margin-left: 44px;
-
- .left-connector {
- @include flat-connector-before;
- }
- }
- }
-
- &.no-margin {
- margin: 0;
- }
-
- li {
- list-style: none;
- }
-
- // when downstream pipelines are present, the last stage isn't the last column
- &:last-child:not(.has-downstream) {
- .build {
- // Remove right connecting horizontal line from first build in last stage
- &:first-child::after {
- border: 0;
- }
- // Remove right curved connectors from all builds in last stage
- &:not(:first-child)::after {
- border: 0;
- }
- // Remove opposite curve
- .curve::before {
- display: none;
- }
- }
- }
-
- // when upstream pipelines are present, the first stage isn't the first column
- &:first-child:not(.has-upstream) {
- .build {
- // Remove left curved connectors from all builds in first stage
- &:not(:first-child)::before {
- border: 0;
- }
- // Remove opposite curve
- .curve::after {
- display: none;
- }
- }
- }
-
- // Curve first child connecting lines in opposite direction
- .curve {
- display: none;
-
- &::before,
- &::after {
- content: '';
- width: 21px;
- height: 25px;
- position: absolute;
- top: -31px;
- border-top: 2px solid var(--border-color, $border-color);
- }
-
- &::after {
- left: -44px;
- border-right: 2px solid var(--border-color, $border-color);
- border-radius: 0 20px;
- }
-
- &::before {
- right: -44px;
- border-left: 2px solid var(--border-color, $border-color);
- border-radius: 20px 0 0;
- }
- }
- }
-
- .stage-name {
- margin: 0 0 15px 10px;
- font-weight: $gl-font-weight-bold;
- width: 176px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- line-height: 2.2em;
- }
-
- .build {
- position: relative;
- width: 186px;
- margin-bottom: 10px;
- white-space: normal;
-
- .ci-job-dropdown-container {
- // override dropdown.scss
- .dropdown-menu li button {
- padding: 0;
- text-align: center;
- }
- }
-
- .ci-status-icon svg {
- height: 24px;
- width: 24px;
- }
-
- .dropdown-menu-toggle {
- background-color: transparent;
- border: 0;
- padding: 0;
-
- &:focus {
- outline: none;
- }
- }
-
- .build-content {
- @include build-content();
- }
-
- .ci-job-dropdown-container:hover .build-content,
- a.build-content:hover,
- button.build-content:hover {
- background-color: var(--gray-100, $gray-100);
- border: 1px solid $dropdown-toggle-active-border-color;
- }
-
- // Connect first build in each stage with right horizontal line
- &:first-child {
- &::after {
- content: '';
- position: absolute;
- top: 48%;
- right: -48px;
- border-top: 2px solid var(--border-color, $border-color);
- width: 48px;
- height: 1px;
- }
- }
-
- // Connect each build (except for first) with curved lines
- &:not(:first-child) {
- &::after,
- &::before {
- content: '';
- top: -49px;
- position: absolute;
- border-bottom: 2px solid var(--border-color, $border-color);
- width: 25px;
- height: 69px;
- }
-
- // Right connecting curves
- &::after {
- right: -25px;
- border-right: 2px solid var(--border-color, $border-color);
- border-radius: 0 0 20px;
- }
-
- // Left connecting curves
- &::before {
- left: -25px;
- border-left: 2px solid var(--border-color, $border-color);
- border-radius: 0 0 0 20px;
- }
- }
-
- // Connect second build to first build with smaller curved line
- &:nth-child(2) {
- &::after,
- &::before {
- height: 29px;
- top: -9px;
- }
-
- .curve {
- display: block;
- }
- }
- }
-
- .ci-action-icon-container {
- position: absolute;
- right: 5px;
- top: 50%;
- transform: translateY(-50%);
-
- // Action Icons in big pipeline-graph nodes
- &.ci-action-icon-wrapper {
- height: 30px;
- width: 30px;
- border-radius: 100%;
- display: block;
- padding: 0;
- line-height: 0;
-
- svg {
- fill: var(--gray-500, $gray-500);
- }
-
- .gl-spinner {
- top: 2px;
- }
-
- &.play {
- svg {
- left: 1px;
- top: 1px;
- }
- }
- }
- }
-
- .stage-action svg {
- left: 1px;
- top: -2px;
- }
-}
-
-// Triggers the dropdown in the big pipeline graph
-.dropdown-counter-badge {
- font-weight: 100;
- font-size: 15px;
- position: absolute;
- right: 13px;
- top: 8px;
-}
-
.split-report-section {
border-bottom: 1px solid var(--gray-50, $gray-50);
@@ -480,34 +204,6 @@
left: 100%;
top: -10px;
box-shadow: 0 1px 5px $black-transparent;
-
- /**
- * Top arrow in the dropdown in the big pipeline graph
- */
- &::before,
- &::after {
- content: '';
- display: inline-block;
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
- top: 18px;
- }
-
- &::before {
- left: -6px;
- margin-top: 3px;
- border-width: 7px 5px 7px 0;
- border-right-color: var(--border-color, $border-color);
- }
-
- &::after {
- left: -5px;
- border-width: 10px 7px 10px 0;
- border-right-color: var(--white, $white);
- }
}
.codequality-report {
diff --git a/app/assets/stylesheets/page_bundles/reports.scss b/app/assets/stylesheets/page_bundles/reports.scss
index ce91988cb8a..d0748779f47 100644
--- a/app/assets/stylesheets/page_bundles/reports.scss
+++ b/app/assets/stylesheets/page_bundles/reports.scss
@@ -49,11 +49,6 @@
display: flex;
}
-.is-dismissed .report-block-list-issue-description,
-.is-dismissed .vulnerability-name-button {
- text-decoration: line-through;
-}
-
.report-block-list-issue-description-text::after {
content: '\00a0';
}
diff --git a/app/assets/stylesheets/page_bundles/signup.scss b/app/assets/stylesheets/page_bundles/signup.scss
index d6c3a3ff5da..57e5d2411d1 100644
--- a/app/assets/stylesheets/page_bundles/signup.scss
+++ b/app/assets/stylesheets/page_bundles/signup.scss
@@ -39,36 +39,6 @@
}
}
-.signup-page[data-page^='registrations:experience_levels'] {
- $card-shadow-color: rgba(var(--black, $black), 0.2);
-
- .page-wrap {
- background-color: var(--white, $white);
- }
-
- .card-deck {
- max-width: 828px;
- }
-
- .card {
- transition: box-shadow 0.3s ease-in-out;
- }
-
- .card:hover {
- box-shadow: 0 $gl-spacing-scale-3 $gl-spacing-scale-5 $card-shadow-color;
- }
-
- @media (min-width: $breakpoint-sm) {
- .card-deck .card {
- margin: 0 $gl-spacing-scale-3;
- }
- }
-
- .stretched-link:hover {
- text-decoration: none;
- }
-}
-
.edit-profile {
max-width: 460px;
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 5173aeb824e..bc4dbf695cf 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -250,6 +250,10 @@
.commit-row-description {
display: none;
flex: 1;
+
+ a {
+ color: $blue-600;
+ }
}
&.inline-commit {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ee97e8af296..94912b1c641 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -7,6 +7,7 @@
text-align: center;
margin-right: $issuable-warning-icon-margin;
line-height: $gl-line-height-24;
+ flex: 0 0 auto;
}
.limit-container-width {
@@ -121,7 +122,9 @@
.right-sidebar {
position: fixed;
top: $header-height;
- bottom: 0;
+ // Default value for CSS var must contain a unit
+ // stylelint-disable-next-line length-zero-no-unit
+ bottom: var(--review-bar-height, 0px);
right: 0;
transition: width $sidebar-transition-duration;
background: $gray-light;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index b537a46a6f2..773935f4c76 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -96,15 +96,10 @@
}
form {
- width: 48%;
padding: 0;
border: 0;
background: none;
margin-bottom: $gl-padding;
-
- @include media-breakpoint-down(md) {
- width: 100%;
- }
}
.omniauth-btn {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 34a03a07405..3c0f10eb5cb 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -37,7 +37,7 @@
.note-textarea {
display: block;
- padding: 10px 0;
+ padding: 10px 1px;
color: $gl-text-color;
font-family: $regular-font;
border: 0;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b52a3c445b5..de9e0c6f705 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -70,14 +70,6 @@
}
}
-.profile-link-holder {
- display: inline;
-
- a:not(.text-link) {
- text-decoration: none;
- }
-}
-
// Middle dot divider between each element in a list of items.
.middle-dot-divider {
@include middle-dot-divider;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 8f5de73365b..2e6c6a021f8 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -37,6 +37,25 @@ input[type='checkbox']:hover {
0 0 0 1px lighten($dropdown-input-focus-shadow, 20%);
}
+.header-search {
+ width: 320px;
+
+ input,
+ svg {
+ transition: border-color ease-in-out $default-transition-duration,
+ background-color ease-in-out $default-transition-duration;
+ }
+}
+
+.header-search-dropdown-menu {
+ max-height: $dropdown-max-height;
+ top: $header-height;
+}
+
+.header-search-dropdown-content {
+ max-height: $dropdown-max-height;
+}
+
.search {
margin: 0 8px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index c6198315606..5765156f26c 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -102,7 +102,6 @@
.tree-table {
margin-bottom: 0;
- table-layout: fixed;
tr {
border-bottom: 1px solid $white-normal;
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index a90751f772e..b7958cdf4a3 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -11,6 +11,7 @@ body.gl-dark {
--green-700: #91d4a8;
--blue-400: #1f75cb;
--orange-400: #ab6100;
+ --purple-100: #2f2a6b;
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
--black: #fff;
@@ -374,6 +375,38 @@ h1 {
.m-auto {
margin: auto !important;
}
+.gl-form-input,
+.gl-form-input.form-control {
+ background-color: #333;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ height: auto;
+ color: #fafafa;
+ box-shadow: inset 0 0 0 1px #868686;
+ border-style: none;
+ appearance: none;
+ -moz-appearance: none;
+}
+.gl-form-input:disabled,
+.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
+.gl-form-input.form-control:disabled,
+.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
+ background-color: #1f1f1f;
+ color: #868686;
+ box-shadow: inset 0 0 0 1px #404040;
+ cursor: not-allowed;
+}
+.gl-form-input::placeholder,
+.gl-form-input.form-control::placeholder {
+ color: #868686;
+}
.gl-button {
display: inline-flex;
}
@@ -1237,7 +1270,7 @@ input {
.nav-sidebar-inner-scroll > div.context-header a .avatar-container {
font-weight: 400;
flex: none;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.nav-sidebar-inner-scroll > div.context-header a .avatar-container.rect-avatar {
border-style: none;
@@ -1247,7 +1280,7 @@ input {
a
.avatar-container.rect-avatar
.avatar.s32 {
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
margin-top: 0.25rem;
@@ -1261,7 +1294,7 @@ input {
.sidebar-top-level-items .context-header a .avatar-container {
font-weight: 400;
flex: none;
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items .context-header a .avatar-container.rect-avatar {
border-style: none;
@@ -1271,7 +1304,7 @@ input {
a
.avatar-container.rect-avatar
.avatar.s32 {
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items > li .badge.badge-pill {
border-radius: 0.5rem;
@@ -1409,6 +1442,9 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
+.header-search {
+ width: 320px;
+}
.search {
margin: 0 8px;
}
@@ -1479,7 +1515,7 @@ svg.s16 {
float: left;
margin-right: 16px;
border-radius: 50%;
- border: 1px solid rgba(255, 255, 255, 0.08);
+ border: 1px solid rgba(0, 0, 0, 0.08);
}
.avatar.s16,
.avatar-container.s16 {
@@ -1524,7 +1560,7 @@ svg.s16 {
background-color: #660e00;
}
.identicon.bg2 {
- background-color: #f4f0ff;
+ background-color: #232150;
}
.identicon.bg3 {
background-color: #f1f1ff;
@@ -1635,6 +1671,22 @@ body.gl-dark
.notification-dot {
background-color: #fafafa;
}
+body.gl-dark .header-search {
+ background-color: rgba(250, 250, 250, 0.2) !important;
+}
+body.gl-dark .header-search svg {
+ color: rgba(250, 250, 250, 0.8) !important;
+}
+body.gl-dark .header-search input {
+ background-color: transparent;
+ color: rgba(250, 250, 250, 0.8);
+}
+body.gl-dark .header-search input::placeholder {
+ color: rgba(250, 250, 250, 0.8);
+}
+body.gl-dark .header-search input:active::placeholder {
+ color: #fafafa;
+}
body.gl-dark .search form {
background-color: rgba(250, 250, 250, 0.2);
}
@@ -1651,7 +1703,7 @@ body.gl-dark .nav-sidebar li.active > a {
body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
- background-color: #2f2a6b;
+ background-color: var(--purple-100, #e1d8f9);
color: var(--black, #333);
}
body.gl-dark .logo-text svg {
@@ -1668,6 +1720,14 @@ body.gl-dark .navbar-gitlab .navbar-nav li.active > button {
color: var(--gl-text-color);
background-color: var(--gray-200);
}
+body.gl-dark .navbar-gitlab .header-search {
+ background-color: var(--gray-100) !important;
+ box-shadow: inset 0 0 0 1px var(--border-color) !important;
+}
+body.gl-dark .navbar-gitlab .header-search:active {
+ background-color: var(--gray-100) !important;
+ box-shadow: inset 0 0 0 1px var(--blue-200) !important;
+}
body.gl-dark .navbar-gitlab .search form {
background-color: var(--gray-100);
box-shadow: inset 0 0 0 1px var(--border-color);
@@ -1746,6 +1806,17 @@ body.gl-dark {
--indigo-900: #ebebfa;
--indigo-950: #f7f7ff;
--indigo-900-alpha-008: rgba(235, 235, 250, 0.08);
+ --purple-50: #232150;
+ --purple-100: #2f2a6b;
+ --purple-200: #453894;
+ --purple-300: #5943b6;
+ --purple-400: #694cc0;
+ --purple-500: #7b58cf;
+ --purple-600: #9475db;
+ --purple-700: #ac93e6;
+ --purple-800: #cbbbf2;
+ --purple-900: #e1d8f9;
+ --purple-950: #f4f0ff;
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
--white: #333;
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 0b2d34b6f5d..2c79b819899 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -355,6 +355,38 @@ h1 {
.m-auto {
margin: auto !important;
}
+.gl-form-input,
+.gl-form-input.form-control {
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-size: 0.875rem;
+ line-height: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ height: auto;
+ color: #303030;
+ box-shadow: inset 0 0 0 1px #868686;
+ border-style: none;
+ appearance: none;
+ -moz-appearance: none;
+}
+.gl-form-input:disabled,
+.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
+.gl-form-input.form-control:disabled,
+.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
+ background-color: #fafafa;
+ color: #868686;
+ box-shadow: inset 0 0 0 1px #dbdbdb;
+ cursor: not-allowed;
+}
+.gl-form-input::placeholder,
+.gl-form-input.form-control::placeholder {
+ color: #868686;
+}
.gl-button {
display: inline-flex;
}
@@ -1390,6 +1422,9 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
+.header-search {
+ width: 320px;
+}
.search {
margin: 0 8px;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 070ab36e0b3..013ad3fac87 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -315,9 +315,9 @@ fieldset:disabled a.btn {
-moz-appearance: none;
}
.gl-form-input:disabled,
-.gl-form-input:not(.form-control-plaintext):read-only,
+.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
.gl-form-input.form-control:disabled,
-.gl-form-input.form-control:not(.form-control-plaintext):read-only {
+.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
background-color: #fafafa;
color: #868686;
box-shadow: inset 0 0 0 1px #dbdbdb;
@@ -634,17 +634,11 @@ svg {
margin: 0;
}
.login-page .omniauth-container form {
- width: 48%;
padding: 0;
border: 0;
background: none;
margin-bottom: 16px;
}
-@media (max-width: 991.98px) {
- .login-page .omniauth-container form {
- width: 100%;
- }
-}
.login-page .omniauth-container .omniauth-btn {
width: 100%;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 8e1438eaf8a..f12b2ee2591 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -72,6 +72,18 @@ $indigo-900: #ebebfa;
$indigo-950: #f7f7ff;
$indigo-900-alpha-008: rgba($indigo-900, 0.08);
+$purple-50: #232150;
+$purple-100: #2f2a6b;
+$purple-200: #453894;
+$purple-300: #5943b6;
+$purple-400: #694cc0;
+$purple-500: #7b58cf;
+$purple-600: #9475db;
+$purple-700: #ac93e6;
+$purple-800: #cbbbf2;
+$purple-900: #e1d8f9;
+$purple-950: #f4f0ff;
+
$gray-lightest: #222;
$gray-light: $gray-50;
$gray-lighter: #303030;
@@ -163,6 +175,18 @@ body.gl-dark {
--indigo-950: #{$indigo-950};
--indigo-900-alpha-008: #{$indigo-900-alpha-008};
+ --purple-50: #{$purple-50};
+ --purple-100: #{$purple-100};
+ --purple-200: #{$purple-200};
+ --purple-300: #{$purple-300};
+ --purple-400: #{$purple-400};
+ --purple-500: #{$purple-500};
+ --purple-600: #{$purple-600};
+ --purple-700: #{$purple-700};
+ --purple-800: #{$purple-800};
+ --purple-900: #{$purple-900};
+ --purple-950: #{$purple-950};
+
--gl-text-color: #{$gray-900};
--border-color: #{$border-color};
@@ -252,6 +276,10 @@ $well-inner-border: $gray-200;
color: $gray-900;
}
+.gl-label-text-dark.gl-label-text-dark {
+ color: $gray-10;
+}
+
// This applies to "gl-labels" from "gitlab-ui"
.gl-label.gl-label-scoped.gl-label-text-dark,
.gl-label.gl-label-scoped.gl-label-text-light {
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index a94169ab494..a9e8b238d78 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -140,6 +140,34 @@
}
}
+ .header-search {
+ background-color: rgba($search-and-nav-links, 0.2) !important;
+
+ &:hover {
+ background-color: rgba($search-and-nav-links, 0.3) !important;
+ }
+
+ svg {
+ color: rgba($search-and-nav-links, 0.8) !important;
+ }
+
+ input {
+ background-color: transparent;
+ color: rgba($search-and-nav-links, 0.8);
+
+ &::placeholder {
+ color: rgba($search-and-nav-links, 0.8);
+ }
+
+ &:focus,
+ &:active {
+ &::placeholder {
+ color: $search-and-nav-links;
+ }
+ }
+ }
+ }
+
.search {
form {
background-color: rgba($search-and-nav-links, 0.2);
@@ -184,7 +212,7 @@
a:hover,
&.active a,
.fly-out-top-item-container {
- background-color: $purple-900;
+ background-color: var(--purple-100, $purple-900);
color: var(--black, $white);
}
}
diff --git a/app/assets/stylesheets/themes/theme_light.scss b/app/assets/stylesheets/themes/theme_light.scss
index b41377475c5..4c3bc1b2298 100644
--- a/app/assets/stylesheets/themes/theme_light.scss
+++ b/app/assets/stylesheets/themes/theme_light.scss
@@ -45,6 +45,16 @@ body {
}
}
+ .header-search {
+ background-color: $white !important;
+ box-shadow: inset 0 0 0 1px $border-color !important;
+
+ &:hover {
+ background-color: $white !important;
+ box-shadow: inset 0 0 0 1px $blue-200 !important;
+ }
+ }
+
.search {
form {
background-color: $white;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index ccad503c1ed..ec70926b418 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -245,11 +245,16 @@ $gl-line-height-42: px-to-rem(42px);
width: $grid-size * 28;
}
-// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1491
+// Will be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2347 is merged
.gl-min-w-8 {
min-width: $gl-spacing-scale-8;
}
+// Will be removed after https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/2347 is merged
+.gl-min-w-10 {
+ min-width: $gl-spacing-scale-10;
+}
+
// Will both be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1526
.gl-opacity-6 {
opacity: 0.6;
@@ -258,3 +263,21 @@ $gl-line-height-42: px-to-rem(42px);
.gl-opacity-7 {
opacity: 0.7;
}
+
+/**
+ Note: ::-webkit-scrollbar is a non-standard rule only
+ supported by webkit browsers.
+
+ It is added here to migrate components that use
+ scrolling-links() mixin from `app/assets/stylesheets/framework/mixins.scss`.
+
+ It should not be used elsewhere: it may impact accessibility as well as
+ add browser compatibility issues.
+
+ See: https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar
+**/
+.gl-webkit-scrollbar-display-none {
+ &::-webkit-scrollbar {
+ display: none;
+ }
+}